Adobe Acrobat 拡張機能が有効な端末で、POST で返した PDF が 405 になる

POST のレスポンスとして PDF を返す機能が、特定の端末でのみ 405 Method Not Allowed になりました。原因は Adobe Acrobat の Chrome 拡張機能で、アプリケーション側の問題ではありませんでした。

新規タブに 405 が出る

  • PDF を表示する操作で、新規タブに 405 Method Not Allowed が出る
  • 同じアカウント・同じ URL でも、端末によって再現する / しないが分かれる
  • サーバーは無変更。リリースのタイミングとも一致しない

再現する端末には拡張機能が入っていた

再現する端末に共通していたのは、Adobe Acrobat の Chrome 拡張機能が有効になっていることでした。無効にすると解消し、有効に戻すと再現します。

拡張機能が同じ URL を GET で取り直す

この拡張機能は内蔵 PDF ビューアを自前のビューアに置き換えます。その際、受信済みのレスポンスボディを再利用せず、タブの URL を GET で取り直します。

[意図した動作]
  ブラウザ ──POST──► サーバー ──200 application/pdf──► 内蔵ビューアで表示

[拡張機能が有効な場合]
  ブラウザ ──POST──► サーバー ──200 application/pdf──► 拡張機能が横取り
                                                        │
                      サーバー ◄──GET(同じ URL)───────┘  ← メソッドが変わる
                         │
                         └──405 Method Not Allowed──► タブに表示される

POST しか受け付けない URL に GET が飛ぶため 405 になります。PDF の取得自体は成功しているものの、拡張機能がその結果を使わずに取り直しています。

次の 4 つが重なったときに起きる

  • PDF を POST のレスポンスとして返している
  • Content-Disposition: inlineattachment ならビューアを経由しないため発生しない)
  • その URL に GET のハンドラがない
  • 端末で該当の拡張機能が有効

GET で取得している PDF では問題になりません。取り直しても同じレスポンスが返ります。

アプリが発行していない GET を見つける

  • 端末で挙動が分かれるなら、原因はアプリの外にある可能性が高い。拡張機能・セキュリティソフト・プロキシが候補
  • 手がかりはアクセスログに残る。アプリが送るはずのない GET が、POST の直後に同じ URL へ記録されている
  • 405 は URL 自体が一致していることを示す。ルーティングは通ったうえでメソッドだけが弾かれている。単一メソッドしか使わない URL でこれが出た場合、アプリ以外が発行していると考えられる

参考

MariaDB 10.6 と 10.10 で TIMESTAMP カラムの暗黙付与を検証する

MariaDB では explicit_defaults_for_timestamp が未設定の場合、テーブル内で最初のTIMESTAMP 型カラムに NULL 属性・DEFAULT 句・ON UPDATE 句のいずれも指定していないと、DEFAULT current_timestamp()ON UPDATE current_timestamp() が暗黙に付与されます。

検証環境 mariadb10.6とmariadb10.10

Docker で MariaDB 10.6 と 10.10 のコンテナを構築しました。

services:
  mariadb-10.6:
    image: mariadb:10.6
    ports:
      - "3306:3306"
  mariadb-10.10:
    image: mariadb:10.10
    ports:
      - "3307:3306"

実行したSQL

両バージョンで同じテーブルを作成しました。token_expires カラムには NULL 属性・DEFAULT 句・ON UPDATE 句のいずれも指定していません。

CREATE TABLE timestamp_auto_default_test (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  token_expires TIMESTAMP NOT NULL
);

SHOW CREATE TABLE timestamp_auto_default_test;

検証結果

MariaDB 10.6 では、token_expires カラムに DEFAULT current_timestamp()ON UPDATE current_timestamp() が付与されました。

`token_expires` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()

MariaDB 10.10 では、同じ DDL でも付与されませんでした。

`token_expires` timestamp NOT NULL

まとめ

暗黙付与の挙動差を確認できました。

原因は、10.10 でこの機能自体が撤廃されたことではありません。explicit_defaults_for_timestampの値を確認したところ、明示的に設定していないにもかかわらず 10.6 では 0、10.10 では 1 でした。
このシステム変数のデフォルト値が 10.10 で変更されたことが、暗黙付与の有無を分けています。

LaravelのマイグレーションでTIMESTAMP型カラムが意図せず自動更新される

発生した問題

Laravelのマイグレーションで作成したTIMESTAMP型カラムが、明示的に値を指定していないUPDATE実行時に自動更新されるという事象に遭遇しました。原因を調査したので、その内容を共有します。

環境

  • PHP(Laravel)
  • MariaDB 10.10未満

再現コード

以下のマイグレーションでカラムを作成します。

Schema::create('your_table', function (Blueprint $table) {
    $table->id();
    $table->timestamp('token_expires');
});

このテーブルに対してレコードをUPDATEすると、token_expiresを指定していないにもかかわらず値が更新されます。

原因

MariaDBの自動付与仕様

MariaDB 10.10未満では、システム変数explicit_defaults_for_timestampが未設定の場合(これがデフォルト値)、テーブル内で最初に定義されたTIMESTAMP型カラムに対し、NULL属性、DEFAULT句、ON UPDATE句のいずれも明示していなければ、次の属性が自動的に付与されます。

  • DEFAULT current_timestamp()
  • ON UPDATE current_timestamp()

この仕様には、押さえておくべきポイントが2つあります。

  1. 対象になるのは最初のTIMESTAMP型カラムのみです。2番目以降のTIMESTAMP型カラムは、DEFAULT句を省略していても自動付与の対象にはなりません。
  2. 「肩代わり」は発生しません。先頭のTIMESTAMP型カラムが何らかの理由(NULL属性の明示など)で対象外だった場合でも、その特別扱いが次のカラムに移ることはありません。

今回のケースでは、token_expiresがテーブル内で最初に定義されたTIMESTAMP型カラムだったため、この自動付与の対象になったと考えられます。

なお、$table->timestamps()created_at/updated_atを先に定義していれば、話は変わります。Laravelはこれらをnullable()付き、つまりNULL属性を明示した状態で生成するため、自動付与の対象から除外され、後続のカラムにも影響しません。

Laravel側の挙動

$table->timestamp('token_expires')と書いた場合、Laravelはnullable()を指定しない限りNOT NULLを付与しますが、DEFAULT句については何も出力しません。

結果として、MariaDBに送られるSQLは次のようになります。

CREATE TABLE `your_table` (
  `id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  `token_expires` TIMESTAMP NOT NULL,
  PRIMARY KEY (`id`)
);

NOT NULLは指定されていても、DEFAULT句、ON UPDATE句が明示されていないため、token_expiresが最初のTIMESTAMP型カラムであるこのテーブルでは、MariaDB(10.10未満)側がDEFAULT current_timestamp() ON UPDATE current_timestamp()を自動で埋め込んでしまいます。

まとめ

  • MariaDB 10.10未満は、explicit_defaults_for_timestampが未設定だと、テーブル内最初のTIMESTAMP型カラムにDEFAULT/ON UPDATE句を自動付与する仕様を持っています。
  • 対象になるのは先頭のTIMESTAMP型カラムのみで、対象外になった分が後続カラムに引き継がれることはありません。
  • Laravelの$table->timestamp()はDEFAULT句を出力しないため、意図せずこの自動付与仕様の対象になり得ます。
  • $table->timestamps()created_at/updated_atを先に定義しておくと、これらがNULL許容で生成されるため、後続のTIMESTAMP型カラムを自動付与仕様から守ることができます。

参考

Md2Backlogを更新しました。

Md2Backlogを更新しました。
Backlog記法へ変換する際のエスケープ処理に不具合があり、修正しました。
特殊文字が正しくエスケープされ、誤変換されなくなりました。