vscode.dev においてAsciiDocプレビュー内の画像が net::ERR_BLOCKED_BY_RESPONSE エラーで表示されない問題

create
2023年12月25日
update
2023年12月25日

🤢症状

AsciiDoc 文書作成中に kroki でグラフ描画しようとしたらプレビュー画面に表示されなかった。
ChromeDevelopper Tools からコンソールエラーを見てみると net::ERR_BLOCKED_BY_RESPONSE.NotSameOriginAfterDefaultedToSameOriginByCoep のエラー表示。

Table 1. 環境
App Version

VS Code

1.84.1

AsciiDoc (VS Code 拡張機能)

v3.1.7

🔍原因

vscode.dev ドメインを介してアクセスしてるので CORS 問題か?
と思ったら Cross-Origin-Resource-Policy(CORP) レスポンスヘッダー未定義によるエラーみたい。

CORP未定義によるエラー。

上記エラーは Cross-Origin-Embedder-Policy(COEP) レスポンスヘッダーにおいて、明示的に許可を与えていない外部 origin のリソースが読み込まれることを防止する設定[1]の場合に発生する。
つまり vscode.dev 側の COEP 設定に起因する。

よって当然これは kroki サービスだけの問題ではなく、他の CORP レスポンスヘッダーが定義されていないサービスもブロックされる。

🚑対策

とりあえず今回は vscode.dev 上のプレビューで画像ブロックされてしまうことを回避できればいいので、 <img> タグに crossorigin 属性をつけることにした[2]

Example 1. crossorigin 属性
<img src="https://example.com/hoge.png">  (1)

<img src="https://example.com/hoge.png" crossorigin>  (2)
1 COEP ブロックされる。
2 <img><link> タグなどに crossorigin 属性をつけると COEP によるブロックは回避できる(CORS 対応の必要あり)。
📖 HTML 属性: crossorigin - HTML: ハイパーテキストマークアップ言語 | MDN

なお AsciiDoc の基本機能では自由に HTML 属性を追加することはできないため、Asciidoc.js のカスタマイズ機能を利用する。

Asciidoctor.js Extensionscrossorigin 属性を追加する

asciidoctor-vscode ではカスタマイズ機能の一つである Asciidoctor.js Extensions が利用できる[3]ため、それにより crossorigin 属性を自動で追加するようにした。

Example 2. asciidoctor-vscode で Asciidoctor.js Extensions の利用

方法としては Postprocessor でHTML変換後の文字列を正規表現で置換させる。
📖 Postprocessor Extension Example | Asciidoctor Docs

  1. Asciidoctor.js の拡張機能を有効化する

    .vscode/settings.json
    {
      "asciidoc.extensions.registerWorkspaceExtensions": true
    }
  2. .asciidoctor/lib ディレクトリ下に拡張機能(*.js)ファイルを作成する

    crossorigin 属性を追加する拡張機能ファイルの例
    .asciidoctor/lib/crossorigin.js
    // @ts-check
    /**
     * @typedef {object} CrossoriginTarget - crossorigin 属性をつけたい対象の情報を格納。
     * @prop {string[]} urls - 置換対象とするURL
     * @prop {'audio'|'img'|'link'|'script'|'video'} tag - 置換対象とするHTMLタグ名
     */
    
    /**
     * @typedef {*} Postprocessor
     *
     * @typedef {object} PostprocessorDsl
     * @prop {(block: (this: Postprocessor, document: Document, output: string) => void) => void} process
     */
    
    /**
     * 置換対象とする正規表現パターンを作成。
     * @param {CrossoriginTarget} target
     * @returns {RegExp}
     */
    const createPattern = (target) => {
      const orUrl = `(?:${target.urls.join('|')})`
    
      switch (target.tag) {
        case 'img':
          return new RegExp(`(<img .*?src="${orUrl}[^>]*)>`, 'g')
    
        default:
          console.warn(
            `[crossorigin Extension]: ${target.tag} tag is not implements.`
          )
          break
      }
    
      return new RegExp('')
    }
    
    /**
     * Postprocessor 拡張機能の実装。
     * 外部参照の <img> タグに crossorigin 属性を挿入する。
     * @this {PostprocessorDsl}
     */
    function processor() {
      /** @type {CrossoriginTarget[]} */
      const targets = [
        {
          tag: 'img',
          urls: ['https://img.shields.io', 'https://kroki.io', 'http://localhost'],
        },
      ]
    
      const self = this
      self.process((doc, output) => {
        const replaced = targets
          .map((target) => createPattern(target))
          .reduce(
            (source, pattern) => source.replace(pattern, '$1 crossorigin>'),
            output
          )
    
        return replaced
      })
    }
    
    /**
     * 上記 processor() を Postprocessor として登録する register 関数を公開。
     * @param {*} registry
     */
    module.exports.register = (registry) => {
      typeof registry.postprocessor === 'function'
        ? registry.postprocessor(processor)
        : console.warn(
            '[crossorigin Extension]: registry.postprocessor is not function.'
          )
    }

以上で作成した拡張機能が適用される。


1. require-corp
2. 本来は kroki サーバー側で Cross-Origin-Resource-Policy: cross-origin レスポンスヘッダーを設定することが一番筋がよさそう。ただ、kroki 側の設定で CORP レスポンスヘッダーを追加する仕組みはなさそうだし、ローカルサーバー立ててあれこれしても外部アクセスできるメリットが意味なくなるし……。
3. Template Converter も使えるはずだけど vscode.dev 上ではエラーが出たので見送り。