📌

SSR StreamingをHTTPフレーム単位で見る

2023/12/08に公開

概要

SSR Streamingは、使いやすく素晴らしい機能ですが、
その通信がどのような仕組みで動いてるかの記事が少ないため、HTTPフレームの通信を見ていきます。
また、この記事では、HTTP/1.1での通信は行わず、HTTP/2のみ関心を寄せています。

(本来は、HTTP/3の通信も確認したいですが、vercelがHTTP/2のみ対応しているので...)

Streaming

まず、大前提としてストリーミングですが、
今回はNext.js 14のストリーミングを基準に考えていきます。

https://nextjs.org/docs/app/building-your-application/routing/loading-ui-and-streaming

Google Chrome上で確認すると、画像のようにダウンロードが長く見えるのが特徴です。

サーバー側でのレンダリング処理がデータフェッチなどの非同期処理によって長時間かかる場合、
ブラウザ側ではHTMLを受け取れず、ブラウザ側はネットワークもCPUなどのリソースを持て余してしまいます。
例えば、Early Hintsなどを利用しなければpreloadリソースなどの、リソースの先読みが行えません。

そのような時に、重たい処理をsuspenseでwrapし、ストリーミングを行えるようにすると、すぐにブラウザ側にHTMLを送信することができ、無駄な待ち時間の解消につながります。

streamingの通信を見る

https://streaming-frame-test.vercel.app/
/streaming/[ms]/でms秒を指定することができます。

ザックリ実装されている。アプリの解説をします。

streaming-frame-test/streaming/[ms]/の説明

https://github.com/imamiya-masaki/streaming-frame-test/blob/main/src/app/streaming/[ms]/page.tsx#L6-L8

https://github.com/imamiya-masaki/streaming-frame-test/blob/main/src/app/api/route.ts

フロント側から受け取った[ms]をそのままAPIへリクエストし、
API側では、指定ms、sleep処理します

https://github.com/imamiya-masaki/streaming-frame-test/blob/main/src/app/streaming/[ms]/page.tsx#L25

また、/streaming/[ms]/では、
サーバーキャッシュを行わないようにしています

chromeの開発者ツールでの確認

ms=0の時と、ms=500の違いでは、コンテンツのダウンロードがその分伸びているのがわかります。

nghttp2でみる

https://nghttp2.org/

nghttp2は、HTTP/2の通信をHTTP/2フレーム単位で見ることができます。

brew install nghttp2
nghttp -nva https://streaming-frame-test.vercel.app/dynamic/500
実行結果
nghttp -nva https://streaming-frame-test.vercel.app/streaming/500
[  0.032] Connected
The negotiated protocol: h2
[  0.050] send SETTINGS frame <length=12, flags=0x00, stream_id=0>
          (niv=2)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
          [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
[  0.050] send PRIORITY frame <length=5, flags=0x00, stream_id=3>
          (dep_stream_id=0, weight=201, exclusive=0)
[  0.050] send PRIORITY frame <length=5, flags=0x00, stream_id=5>
          (dep_stream_id=0, weight=101, exclusive=0)
[  0.050] send PRIORITY frame <length=5, flags=0x00, stream_id=7>
          (dep_stream_id=0, weight=1, exclusive=0)
[  0.050] send PRIORITY frame <length=5, flags=0x00, stream_id=9>
          (dep_stream_id=7, weight=1, exclusive=0)
[  0.050] send PRIORITY frame <length=5, flags=0x00, stream_id=11>
          (dep_stream_id=3, weight=1, exclusive=0)
[  0.050] send HEADERS frame <length=61, flags=0x25, stream_id=13>
          ; END_STREAM | END_HEADERS | PRIORITY
          (padlen=0, dep_stream_id=11, weight=16, exclusive=0)
          ; Open new stream
          :method: GET
          :path: /streaming/500
          :scheme: https
          :authority: streaming-frame-test.vercel.app
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.58.0
[  0.057] recv SETTINGS frame <length=30, flags=0x00, stream_id=0>
          (niv=5)
          [SETTINGS_MAX_FRAME_SIZE(0x05):1048576]
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):250]
          [SETTINGS_MAX_HEADER_LIST_SIZE(0x06):2097472]
          [SETTINGS_HEADER_TABLE_SIZE(0x01):4096]
          [SETTINGS_INITIAL_WINDOW_SIZE(0x04):1048576]
[  0.057] recv WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
          (window_size_increment=983041)
[  0.057] recv SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  0.057] send SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  1.264] recv (stream_id=13) :status: 200
[  1.264] recv (stream_id=13) age: 0
[  1.264] recv (stream_id=13) cache-control: private, no-cache, no-store, max-age=0, must-revalidate
[  1.264] recv (stream_id=13) content-encoding: gzip
[  1.264] recv (stream_id=13) content-type: text/html; charset=utf-8
[  1.264] recv (stream_id=13) date: Tue, 05 Dec 2023 19:53:10 GMT
[  1.264] recv (stream_id=13) server: Vercel
[  1.264] recv (stream_id=13) strict-transport-security: max-age=63072000; includeSubDomains; preload
[  1.264] recv (stream_id=13) vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url
[  1.264] recv (stream_id=13) x-matched-path: /streaming/[ms]
[  1.264] recv (stream_id=13) x-powered-by: Next.js
[  1.264] recv (stream_id=13) x-vercel-cache: MISS
[  1.264] recv (stream_id=13) x-vercel-execution-region: iad1
[  1.264] recv (stream_id=13) x-vercel-id: hnd1::iad1::55gcs-1701805988927-5da5bfba3555
[  1.264] recv HEADERS frame <length=311, flags=0x04, stream_id=13>
          ; END_HEADERS
          (padlen=0)
          ; First response header
[  1.264] recv DATA frame <length=750, flags=0x00, stream_id=13>
[  1.264] recv DATA frame <length=1108, flags=0x00, stream_id=13>
[  1.264] send HEADERS frame <length=49, flags=0x25, stream_id=15>
          ; END_STREAM | END_HEADERS | PRIORITY
          (padlen=0, dep_stream_id=11, weight=32, exclusive=0)
          ; Open new stream
          :method: GET
          :path: /_next/static/media/c9a5bc6a7c948fb0-s.p.woff2
          :scheme: https
          :authority: streaming-frame-test.vercel.app
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.58.0
[  1.264] send HEADERS frame <length=40, flags=0x25, stream_id=17>
          ; END_STREAM | END_HEADERS | PRIORITY
          (padlen=0, dep_stream_id=3, weight=32, exclusive=0)
          ; Open new stream
          :method: GET
          :path: /_next/static/css/e7d3bab42d9af29d.css
          :scheme: https
          :authority: streaming-frame-test.vercel.app
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.58.0
[  1.264] send HEADERS frame <length=48, flags=0x25, stream_id=19>
          ; END_STREAM | END_HEADERS | PRIORITY
          (padlen=0, dep_stream_id=5, weight=32, exclusive=0)
          ; Open new stream
          :method: GET
          :path: /_next/static/chunks/webpack-d9f8a4f0dd52fadf.js
          :scheme: https
          :authority: streaming-frame-test.vercel.app
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.58.0
[  1.265] send HEADERS frame <length=48, flags=0x25, stream_id=21>
          ; END_STREAM | END_HEADERS | PRIORITY
          (padlen=0, dep_stream_id=3, weight=32, exclusive=0)
          ; Open new stream
          :method: GET
          :path: /_next/static/chunks/fd9d1056-7b52db27cfdaff1f.js
          :scheme: https
          :authority: streaming-frame-test.vercel.app
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.58.0
[  1.265] send HEADERS frame <length=45, flags=0x25, stream_id=23>
          ; END_STREAM | END_HEADERS | PRIORITY
          (padlen=0, dep_stream_id=3, weight=32, exclusive=0)
          ; Open new stream
          :method: GET
          :path: /_next/static/chunks/472-b67f79dbdd2c1fe1.js
          :scheme: https
          :authority: streaming-frame-test.vercel.app
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.58.0
[  1.275] send HEADERS frame <length=48, flags=0x25, stream_id=25>
          ; END_STREAM | END_HEADERS | PRIORITY
          (padlen=0, dep_stream_id=3, weight=32, exclusive=0)
          ; Open new stream
          :method: GET
          :path: /_next/static/chunks/main-app-892c3dff08e9cd4c.js
          :scheme: https
          :authority: streaming-frame-test.vercel.app
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.58.0
[  1.275] send HEADERS frame <length=49, flags=0x25, stream_id=27>
          ; END_STREAM | END_HEADERS | PRIORITY
          (padlen=0, dep_stream_id=3, weight=32, exclusive=0)
          ; Open new stream
          :method: GET
          :path: /_next/static/chunks/polyfills-c67a75d1b6f99dc8.js
          :scheme: https
          :authority: streaming-frame-test.vercel.app
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.58.0
[  1.286] recv (stream_id=19) :status: 200
[  1.286] recv (stream_id=19) access-control-allow-origin: *
[  1.286] recv (stream_id=19) age: 172821
[  1.286] recv (stream_id=19) cache-control: public,max-age=31536000,immutable
[  1.286] recv (stream_id=19) content-disposition: inline; filename="webpack-d9f8a4f0dd52fadf.js"
[  1.286] recv (stream_id=19) content-encoding: gzip
[  1.286] recv (stream_id=19) content-type: application/javascript; charset=utf-8
[  1.286] recv (stream_id=19) date: Tue, 05 Dec 2023 19:53:10 GMT
[  1.286] recv (stream_id=19) etag: W/"f550bbe13e984c7ddb673492f04171d0"
[  1.286] recv (stream_id=19) server: Vercel
[  1.286] recv (stream_id=19) strict-transport-security: max-age=63072000; includeSubDomains; preload
[  1.286] recv (stream_id=19) x-matched-path: /_next/static/chunks/webpack-d9f8a4f0dd52fadf.js
[  1.286] recv (stream_id=19) x-vercel-cache: HIT
[  1.286] recv (stream_id=19) x-vercel-id: hnd1::sszdh-1701805990152-25c867bb0852
[  1.286] recv HEADERS frame <length=209, flags=0x04, stream_id=19>
          ; END_HEADERS
          (padlen=0)
          ; First response header
[  1.287] recv DATA frame <length=1643, flags=0x00, stream_id=19>
[  1.287] recv (stream_id=25) :status: 200
[  1.287] recv (stream_id=25) accept-ranges: bytes
[  1.287] recv (stream_id=25) access-control-allow-origin: *
[  1.287] recv (stream_id=25) age: 172821
[  1.287] recv (stream_id=25) cache-control: public,max-age=31536000,immutable
[  1.287] recv (stream_id=25) content-disposition: inline; filename="main-app-892c3dff08e9cd4c.js"
[  1.287] recv (stream_id=25) content-type: application/javascript; charset=utf-8
[  1.287] recv (stream_id=25) date: Tue, 05 Dec 2023 19:53:10 GMT
[  1.287] recv (stream_id=25) etag: "c9a92bcd7028363edf7f2ff618d57922"
[  1.287] recv (stream_id=25) server: Vercel
[  1.287] recv (stream_id=25) strict-transport-security: max-age=63072000; includeSubDomains; preload
[  1.287] recv (stream_id=25) x-matched-path: /_next/static/chunks/main-app-892c3dff08e9cd4c.js
[  1.287] recv (stream_id=25) x-vercel-cache: HIT
[  1.287] recv (stream_id=25) x-vercel-id: hnd1::frxb8-1701805990152-8237c55906be
[  1.287] recv (stream_id=25) content-length: 463
[  1.287] recv HEADERS frame <length=154, flags=0x04, stream_id=25>
          ; END_HEADERS
          (padlen=0)
          ; First response header
[  1.287] recv DATA frame <length=0, flags=0x01, stream_id=19>
          ; END_STREAM
[  1.287] recv DATA frame <length=463, flags=0x00, stream_id=25>
[  1.287] recv DATA frame <length=0, flags=0x01, stream_id=25>
          ; END_STREAM
[  1.288] recv (stream_id=17) :status: 200
[  1.288] recv (stream_id=17) access-control-allow-origin: *
[  1.288] recv (stream_id=17) age: 172821
[  1.288] recv (stream_id=17) cache-control: public,max-age=31536000,immutable
[  1.288] recv (stream_id=17) content-disposition: inline; filename="e7d3bab42d9af29d.css"
[  1.288] recv (stream_id=17) content-encoding: gzip
[  1.288] recv (stream_id=17) content-type: text/css; charset=utf-8
[  1.288] recv (stream_id=17) date: Tue, 05 Dec 2023 19:53:10 GMT
[  1.288] recv (stream_id=17) etag: W/"8fd6a223d7ac13f3dfc7bde4e2de01cb"
[  1.288] recv (stream_id=17) server: Vercel
[  1.288] recv (stream_id=17) strict-transport-security: max-age=63072000; includeSubDomains; preload
[  1.288] recv (stream_id=17) x-matched-path: /_next/static/css/e7d3bab42d9af29d.css
[  1.288] recv (stream_id=17) x-vercel-cache: HIT
[  1.288] recv (stream_id=17) x-vercel-id: hnd1::d4d2d-1701805990152-db8589e45365
[  1.288] recv HEADERS frame <length=149, flags=0x04, stream_id=17>
          ; END_HEADERS
          (padlen=0)
          ; First response header
[  1.288] recv DATA frame <length=615, flags=0x00, stream_id=17>
[  1.288] recv DATA frame <length=0, flags=0x01, stream_id=17>
          ; END_STREAM
[  1.288] recv (stream_id=15) :status: 200
[  1.288] recv (stream_id=15) accept-ranges: bytes
[  1.288] recv (stream_id=15) access-control-allow-origin: *
[  1.299] recv (stream_id=15) age: 172821
[  1.299] recv (stream_id=15) cache-control: public,max-age=31536000,immutable
[  1.299] recv (stream_id=15) content-disposition: inline; filename="c9a5bc6a7c948fb0-s.p.woff2"
[  1.299] recv (stream_id=15) content-type: font/woff2
[  1.299] recv (stream_id=15) date: Tue, 05 Dec 2023 19:53:10 GMT
[  1.299] recv (stream_id=15) etag: "74c3556b9dad12fb76f84af53ba69410"
[  1.299] recv (stream_id=15) server: Vercel
[  1.299] recv (stream_id=15) strict-transport-security: max-age=63072000; includeSubDomains; preload
[  1.299] recv (stream_id=15) x-matched-path: /_next/static/media/c9a5bc6a7c948fb0-s.p.woff2
[  1.299] recv (stream_id=15) x-vercel-cache: HIT
[  1.299] recv (stream_id=15) x-vercel-id: hnd1::frxb8-1701805990152-bbfbb030acf6
[  1.299] recv (stream_id=15) content-length: 46552
[  1.299] recv HEADERS frame <length=156, flags=0x04, stream_id=15>
          ; END_HEADERS
          (padlen=0)
          ; First response header
[  1.299] recv DATA frame <length=3513, flags=0x00, stream_id=15>
[  1.299] recv DATA frame <length=16384, flags=0x00, stream_id=15>
[  1.299] recv DATA frame <length=16384, flags=0x00, stream_id=15>
[  1.299] recv DATA frame <length=10271, flags=0x00, stream_id=15>
[  1.299] recv DATA frame <length=0, flags=0x01, stream_id=15>
          ; END_STREAM
[  1.299] recv (stream_id=27) :status: 200
[  1.299] recv (stream_id=27) access-control-allow-origin: *
[  1.299] recv (stream_id=27) age: 2092
[  1.299] recv (stream_id=27) cache-control: public,max-age=31536000,immutable
[  1.299] recv (stream_id=27) content-disposition: inline; filename="polyfills-c67a75d1b6f99dc8.js"
[  1.299] recv (stream_id=27) content-encoding: gzip
[  1.299] recv (stream_id=27) content-type: application/javascript; charset=utf-8
[  1.299] recv (stream_id=27) date: Tue, 05 Dec 2023 19:53:10 GMT
[  1.299] recv (stream_id=27) etag: W/"837c0df77fd5009c9e46d446188ecfd0"
[  1.299] recv (stream_id=27) server: Vercel
[  1.299] recv (stream_id=27) strict-transport-security: max-age=63072000; includeSubDomains; preload
[  1.299] recv (stream_id=27) x-matched-path: /_next/static/chunks/polyfills-c67a75d1b6f99dc8.js
[  1.299] recv (stream_id=27) x-vercel-cache: HIT
[  1.299] recv (stream_id=27) x-vercel-id: hnd1::mc7xd-1701805990152-7a229614cc4b
[  1.299] recv HEADERS frame <length=150, flags=0x04, stream_id=27>
          ; END_HEADERS
          (padlen=0)
          ; First response header
[  1.299] recv DATA frame <length=14404, flags=0x00, stream_id=27>
[  1.299] recv (stream_id=23) :status: 200
[  1.299] recv (stream_id=23) access-control-allow-origin: *
[  1.299] recv (stream_id=23) age: 172821
[  1.299] recv (stream_id=23) cache-control: public,max-age=31536000,immutable
[  1.299] recv (stream_id=23) content-disposition: inline; filename="472-b67f79dbdd2c1fe1.js"
[  1.299] recv (stream_id=23) content-encoding: gzip
[  1.299] recv (stream_id=23) content-type: application/javascript; charset=utf-8
[  1.299] recv (stream_id=23) date: Tue, 05 Dec 2023 19:53:10 GMT
[  1.299] recv (stream_id=23) etag: W/"cb18edb92ec72f27e6d4e762a70d4128"
[  1.299] recv (stream_id=23) server: Vercel
[  1.315] recv (stream_id=23) strict-transport-security: max-age=63072000; includeSubDomains; preload
[  1.315] recv (stream_id=23) x-matched-path: /_next/static/chunks/472-b67f79dbdd2c1fe1.js
[  1.315] recv (stream_id=23) x-vercel-cache: HIT
[  1.315] recv (stream_id=23) x-vercel-id: hnd1::dxcxr-1701805990152-bcddc3a4871b
[  1.315] recv HEADERS frame <length=139, flags=0x04, stream_id=23>
          ; END_HEADERS
          (padlen=0)
          ; First response header
[  1.315] recv (stream_id=21) :status: 200
[  1.315] recv (stream_id=21) access-control-allow-origin: *
[  1.315] recv (stream_id=21) age: 172821
[  1.315] recv (stream_id=21) cache-control: public,max-age=31536000,immutable
[  1.315] recv (stream_id=21) content-disposition: inline; filename="fd9d1056-7b52db27cfdaff1f.js"
[  1.315] recv (stream_id=21) content-encoding: gzip
[  1.315] recv (stream_id=21) content-type: application/javascript; charset=utf-8
[  1.315] recv (stream_id=21) date: Tue, 05 Dec 2023 19:53:10 GMT
[  1.315] recv (stream_id=21) etag: W/"9dee4994f9e89448ff05c84f6bb40b96"
[  1.315] recv (stream_id=21) server: Vercel
[  1.315] recv (stream_id=21) strict-transport-security: max-age=63072000; includeSubDomains; preload
[  1.315] recv (stream_id=21) x-matched-path: /_next/static/chunks/fd9d1056-7b52db27cfdaff1f.js
[  1.315] recv (stream_id=21) x-vercel-cache: HIT
[  1.315] recv (stream_id=21) x-vercel-id: hnd1::6f7kl-1701805990152-d247e4623042
[  1.315] recv HEADERS frame <length=146, flags=0x04, stream_id=21>
          ; END_HEADERS
          (padlen=0)
          ; First response header
[  1.315] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
          (window_size_increment=40851)
[  1.323] recv DATA frame <length=1980, flags=0x00, stream_id=27>
[  1.323] recv DATA frame <length=16384, flags=0x00, stream_id=23>
[  1.323] recv DATA frame <length=14981, flags=0x00, stream_id=27>
[  1.323] recv DATA frame <length=7506, flags=0x00, stream_id=21>
[  1.323] recv DATA frame <length=0, flags=0x01, stream_id=27>
          ; END_STREAM
[  1.323] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
          (window_size_increment=36954)
[  1.334] recv DATA frame <length=12765, flags=0x00, stream_id=23>
[  1.334] recv DATA frame <length=16384, flags=0x00, stream_id=21>
[  1.334] recv DATA frame <length=7805, flags=0x00, stream_id=21>
[  1.334] recv DATA frame <length=0, flags=0x01, stream_id=23>
          ; END_STREAM
[  1.334] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
          (window_size_increment=36764)
[  1.345] recv DATA frame <length=1073, flags=0x00, stream_id=21>
[  1.345] recv DATA frame <length=16384, flags=0x00, stream_id=21>
[  1.345] recv DATA frame <length=4777, flags=0x00, stream_id=21>
[  1.345] recv DATA frame <length=0, flags=0x01, stream_id=21>
          ; END_STREAM
[  1.345] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
          (window_size_increment=38027)
[  2.476] recv DATA frame <length=28, flags=0x00, stream_id=13>
[  2.476] recv DATA frame <length=11, flags=0x00, stream_id=13>
[  2.493] recv DATA frame <length=327, flags=0x00, stream_id=13>
[  2.493] recv DATA frame <length=17, flags=0x00, stream_id=13>
[  2.493] recv DATA frame <length=10, flags=0x00, stream_id=13>
[  2.493] recv DATA frame <length=0, flags=0x01, stream_id=13>
          ; END_STREAM
[  2.493] send GOAWAY frame <length=8, flags=0x00, stream_id=0>
          (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[])

今回は上記のコマンドを実行します。
オプションやnghttpの詳細の説明は省きます。

send SETTINGS

 nghttp -nva https://streaming-frame-test.vercel.app/streaming/500
[  0.024] Connected
The negotiated protocol: h2
[  0.039] send SETTINGS frame <length=12, flags=0x00, stream_id=0>
          (niv=2)
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):100]
          [SETTINGS_INITIAL_WINDOW_SIZE(0x04):65535]
[  0.048] send PRIORITY frame <length=5, flags=0x00, stream_id=3>
          (dep_stream_id=0, weight=201, exclusive=0)
[  0.048] send PRIORITY frame <length=5, flags=0x00, stream_id=5>
          (dep_stream_id=0, weight=101, exclusive=0)
[  0.048] send PRIORITY frame <length=5, flags=0x00, stream_id=7>
          (dep_stream_id=0, weight=1, exclusive=0)
[  0.048] send PRIORITY frame <length=5, flags=0x00, stream_id=9>
          (dep_stream_id=7, weight=1, exclusive=0)
[  0.048] send PRIORITY frame <length=5, flags=0x00, stream_id=11>
          (dep_stream_id=3, weight=1, exclusive=0)

-nvaで実際のレスポンス結果を非表示にしてくれます
レスポンスと合わせて見たい場合は、オプションを-vaとしてください。

まず、
SETTINGSフレームを送信し、サーバーとのやりとりを開始します。
PRIORITYフレームは従来のHTTP/2での優先度制御であり、今回は扱いません。

参照:
RFC7540 日本語訳
RFC9218

recv HEADERS (HTML)

次にHEADERSフレームを送信します。
これはHTTPリクエストのヘッダーのようなもので、その後にしばらくすると、recv HEADERSします

HTTP/2では、特定のデータのまとまり(ストリーム)がstream_idとしてidが振り分けられます。
例えば、ここのstream_id=13はHTMLを示しています。

ストリームでの通信が開始する場合(リクエストする場合)
Open new streamフラグが設定されます

[  0.050] send HEADERS frame <length=61, flags=0x25, stream_id=13>
          ; END_STREAM | END_HEADERS | PRIORITY
          (padlen=0, dep_stream_id=11, weight=16, exclusive=0)
          ; Open new stream
          :method: GET
          :path: /streaming/500
          :scheme: https
          :authority: streaming-frame-test.vercel.app
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.58.0
[  0.057] recv SETTINGS frame <length=30, flags=0x00, stream_id=0>
          (niv=5)
          [SETTINGS_MAX_FRAME_SIZE(0x05):1048576]
          [SETTINGS_MAX_CONCURRENT_STREAMS(0x03):250]
          [SETTINGS_MAX_HEADER_LIST_SIZE(0x06):2097472]
          [SETTINGS_HEADER_TABLE_SIZE(0x01):4096]
          [SETTINGS_INITIAL_WINDOW_SIZE(0x04):1048576]
[  0.057] recv WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
          (window_size_increment=983041)
[  0.057] recv SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  0.057] send SETTINGS frame <length=0, flags=0x01, stream_id=0>
          ; ACK
          (niv=0)
[  1.264] recv (stream_id=13) :status: 200
[  1.264] recv (stream_id=13) age: 0
[  1.264] recv (stream_id=13) cache-control: private, no-cache, no-store, max-age=0, must-revalidate
[  1.264] recv (stream_id=13) content-encoding: gzip
[  1.264] recv (stream_id=13) content-type: text/html; charset=utf-8
[  1.264] recv (stream_id=13) date: Tue, 05 Dec 2023 19:53:10 GMT
[  1.264] recv (stream_id=13) server: Vercel
[  1.264] recv (stream_id=13) strict-transport-security: max-age=63072000; includeSubDomains; preload
[  1.264] recv (stream_id=13) vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url
[  1.264] recv (stream_id=13) x-matched-path: /streaming/[ms]
[  1.264] recv (stream_id=13) x-powered-by: Next.js
[  1.264] recv (stream_id=13) x-vercel-cache: MISS
[  1.264] recv (stream_id=13) x-vercel-execution-region: iad1
[  1.264] recv (stream_id=13) x-vercel-id: hnd1::iad1::55gcs-1701805988927-5da5bfba3555
[  1.264] recv HEADERS frame <length=311, flags=0x04, stream_id=13>
          ; END_HEADERS
          (padlen=0)
          ; First response header

recv DATA (HTML)

その次に、DATAフレームを取得します。
これはHTTP/2でのデータのやり取りで、stream_id=13ですのでHTMLをフレーム単位で受け取っています。

[  1.264] recv DATA frame <length=750, flags=0x00, stream_id=13>
[  1.264] recv DATA frame <length=1108, flags=0x00, stream_id=13>

試しに、-va オプションで実行すると下記のようになります。
HTMLが断片的に取得されているのがわかります。

`-va`での実行
nghttp -va https://streaming-frame-test.vercel.app/streaming/500
...省略

[  0.464] recv (stream_id=13) :status: 200
[  0.464] recv (stream_id=13) age: 0
[  0.464] recv (stream_id=13) cache-control: private, no-cache, no-store, max-age=0, must-revalidate
[  0.464] recv (stream_id=13) content-encoding: gzip
[  0.464] recv (stream_id=13) content-type: text/html; charset=utf-8
[  0.464] recv (stream_id=13) date: Tue, 05 Dec 2023 20:14:01 GMT
[  0.464] recv (stream_id=13) server: Vercel
[  0.464] recv (stream_id=13) strict-transport-security: max-age=63072000; includeSubDomains; preload
[  0.464] recv (stream_id=13) vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url
[  0.464] recv (stream_id=13) x-matched-path: /streaming/[ms]
[  0.464] recv (stream_id=13) x-powered-by: Next.js
[  0.464] recv (stream_id=13) x-vercel-cache: MISS
[  0.464] recv (stream_id=13) x-vercel-execution-region: iad1
[  0.464] recv (stream_id=13) x-vercel-id: hnd1::iad1::55gcs-1701807241685-c028066d78eb
[  0.464] recv HEADERS frame <length=311, flags=0x04, stream_id=13>
          ; END_HEADERS
          (padlen=0)
          ; First response header

<!DOCTYPE html><html lang="ja"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" href="/_next/static/media/c9a5bc6a7c948fb0-s.p.woff2" as="font" crossorigin="" type="font/woff2"/><link rel="stylesheet" href="/_next/static/css/e7d3bab42d9af29d.css" data-precedence="next"/><link rel="preload" as="script" fetchPriority="low" href="/_next/static/chunks/webpack-d9f8a4f0dd52fadf.js"/><script src="/_next/static/chunks/fd9d1056-7b52db27cfdaff1f.js" async=""></script><script src="/_next/static/chunks/472-b67f79dbdd2c1fe1.js" async=""></script><script src="/_next/static/chunks/main-app-892c3dff08e9cd4c.js" async=""></script><title>streaming-test</title><meta name="description" content="check streaming"/><link rel="icon" href="/favicon.ico" type="image/x-icon" sizes="16x16"/><meta name="next-size-adjust"/><script src="/_next/static/chunks/polyfills-c67a75d1b6f99dc8.js" noModule=""></script></head><body class="__className_e66fe9"><main><div>Streaming-test<!--$?--><template id="B:0"></template><p>Loading...</p><!--/$--></div></main><script src="/_next/static/chunks/webpack-d9f8a4f0dd52fadf.js" async=""></script><script>(self.__next_f=self.__next_f||[]).push([0]);self.__next_f.push([2,null])</script><script>self.__next_f.push([1,"1:HL[\"/_next/static/media/c9a5bc6a7c948fb0-s.p.woff2\",\"font\",{\"crossOrigin\":\"\",\"type\":\"font/woff2\"}]\n2:HL[\"/_next/static/css/e7d3bab42d9af29d.css\",\"style\"]\n0:\"$L3\"\n"])</script>[  0.465] recv DATA frame <length=700, flags=0x00, stream_id=13>

<script>self.__next_f.push([1,"4:I[3728,[],\"\"]\n6:I[9928,[],\"\"]\n7:I[6954,[],\"\"]\n8:I[7264,[],\"\"]\na:\"$Sreact.suspense\"\n"])</script><script>self.__next_f.push([1,"3:[[[\"$\",\"link\",\"0\",{\"rel\":\"stylesheet\",\"href\":\"/_next/static/css/e7d3bab42d9af29d.css\",\"precedence\":\"next\",\"crossOrigin\":\"$undefined\"}]],[\"$\",\"$L4\",null,{\"buildId\":\"mJgjBQrRTVvbQPibOFT9R\",\"assetPrefix\":\"\",\"initialCanonicalUrl\":\"/streaming/500\",\"initialTree\":[\"\",{\"children\":[\"streaming\",{\"children\":[[\"ms\",\"500\",\"d\"],{\"children\":[\"__PAGE__\",{}]}]}]},\"$undefined\",\"$undefined\",true],\"initialHead\":[false,\"$L5\"],\"globalErrorComponent\":\"$6\",\"children\":[null,[\"$\",\"html\",null,{\"lang\":\"ja\",\"children\":[\"$\",\"body\",null,{\"className\":\"__className_e66fe9\",\"children\":[\"$\",\"$L7\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\"],\"loading\":\"$undefined\",\"loadingStyles\":\"$undefined\",\"loadingScripts\":\"$undefined\",\"hasLoading\":false,\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L8\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":[[\"$\",\"title\",null,{\"children\":\"404: This page could not be found.\"}],[\"$\",\"div\",null,{\"style\":{\"fontFamily\":\"system-ui,\\\"Segoe UI\\\",Roboto,Helvetica,Arial,sans-serif,\\\"Apple Color Emoji\\\",\\\"Segoe UI Emoji\\\"\",\"height\":\"100vh\",\"textAlign\":\"center\",\"display\":\"flex\",\"flexDirection\":\"column\",\"alignItems\":\"center\",\"justifyContent\":\"center\"},\"children\":[\"$\",\"div\",null,{\"children\":[[\"$\",\"style\",null,{\"dangerouslySetInnerHTML\":{\"__html\":\"body{color:#000;background:#fff;margin:0}.next-error-h1{border-right:1px solid rgba(0,0,0,.3)}@media (prefers-color-scheme:dark){body{color:#fff;background:#000}.next-error-h1{border-right:1px solid rgba(255,255,255,.3)}}\"}}],[\"$\",\"h1\",null,{\"className\":\"next-error-h1\",\"style\":{\"display\":\"inline-block\",\"margin\":\"0 20px 0 0\",\"padding\":\"0 23px 0 0\",\"fontSize\":24,\"fontWeight\":500,\"verticalAlign\":\"top\",\"lineHeight\":\"49px\"},\"children\":\"404\"}],[\"$\",\"div\",null,{\"style\":{\"display\":\"inline-block\"},\"children\":[\"$\",\"h2\",null,{\"style\":{\"fontSize\":14,\"fontWeight\":400,\"lineHeight\":\"49px\",\"margin\":0},\"children\":\"This page could not be found.\"}]}]]}]}]],\"notFoundStyles\":[],\"initialChildNode\":[\"$\",\"$L7\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\",\"streaming\",\"children\"],\"loading\":\"$undefined\",\"loadingStyles\":\"$undefined\",\"loadingScripts\":\"$undefined\",\"hasLoading\":false,\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L8\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":\"$undefined\",\"notFoundStyles\":\"$undefined\",\"initialChildNode\":[\"$\",\"$L7\",null,{\"parallelRouterKey\":\"children\",\"segmentPath\":[\"children\",\"streaming\",\"children\",[\"ms\",\"500\",\"d\"],\"children\"],\"loading\":\"$undefined\",\"loadingStyles\":\"$undefined\",\"loadingScripts\":\"$undefined\",\"hasLoading\":false,\"error\":\"$undefined\",\"errorStyles\":\"$undefined\",\"errorScripts\":\"$undefined\",\"template\":[\"$\",\"$L8\",null,{}],\"templateStyles\":\"$undefined\",\"templateScripts\":\"$undefined\",\"notFound\":
\"$undefined\",\"notFoundStyles\":\"$undefined\",\"initialChildNode\":[\"$L9\",[\"$\",\"main\",null,{\"children\":[\"$\",\"div\",null,{\"children\":[\"Streaming-test\",[\"$\",\"$a\",n
ull,{\"fallback\":[\"$\",\"p\",null,{\"children\":\"Loading...\"}],\"children\":\"$Lb\"}]]}]}],null],\"childPropSegment\":\"__PAGE__\",\"styles\":null}],\"childPropSegment\":[\"ms\",\
"500\",\"d\"],\"styles\":null}],\"childPropSegment\":\"streaming\",\"styles\":null}]}]}],null]}]]\n"])</script><script>self.__next_f.push([1,"5:[[\"$\",\"meta\",\"0\",{\"name\":\"view
port\",\"content\":\"width=device-width, initial-scale=1\"}],[\"$\",\"meta\",\"1\",{\"charSet\":\"utf-8\"}],[\"$\",\"title\",\"2\",{\"children\":\"streaming-test\"}],[\"$\",\"meta\",\
"3\",{\"name\":\"description\",\"content\":\"check streaming\"}],[\"$\",\"link\",\"4\",{\"rel\":\"icon\",\"href\":\"/favicon.ico\",\"type\":\"image/x-icon\",\"sizes\":\"16x16\"}],[\"$
\",\"meta\",\"5\",{\"name\":\"next-size-adjust\"}]]\n9:null\n"])</script>[  0.465] recv DATA frame <length=1161, flags=0x00, stream_id=13>

send HEADERS (resources)

断片的なHTMLが取得されたことで、rel=preloadなどに紐づいたリソースの取得をリクエストすることができます。

[  1.264] send HEADERS frame <length=49, flags=0x25, stream_id=15>
          ; END_STREAM | END_HEADERS | PRIORITY
          (padlen=0, dep_stream_id=11, weight=32, exclusive=0)
          ; Open new stream
          :method: GET
          :path: /_next/static/media/c9a5bc6a7c948fb0-s.p.woff2
          :scheme: https
          :authority: streaming-frame-test.vercel.app
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.58.0
[  1.264] send HEADERS frame <length=40, flags=0x25, stream_id=17>
          ; END_STREAM | END_HEADERS | PRIORITY
          (padlen=0, dep_stream_id=3, weight=32, exclusive=0)
          ; Open new stream
          :method: GET
          :path: /_next/static/css/e7d3bab42d9af29d.css
          :scheme: https
          :authority: streaming-frame-test.vercel.app
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.58.0
[  1.264] send HEADERS frame <length=48, flags=0x25, stream_id=19>
          ; END_STREAM | END_HEADERS | PRIORITY
          (padlen=0, dep_stream_id=5, weight=32, exclusive=0)
          ; Open new stream
          :method: GET
          :path: /_next/static/chunks/webpack-d9f8a4f0dd52fadf.js
          :scheme: https
          :authority: streaming-frame-test.vercel.app
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.58.0
[  1.265] send HEADERS frame <length=48, flags=0x25, stream_id=21>
          ; END_STREAM | END_HEADERS | PRIORITY
          (padlen=0, dep_stream_id=3, weight=32, exclusive=0)
          ; Open new stream
          :method: GET
          :path: /_next/static/chunks/fd9d1056-7b52db27cfdaff1f.js
          :scheme: https
          :authority: streaming-frame-test.vercel.app
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.58.0
[  1.265] send HEADERS frame <length=45, flags=0x25, stream_id=23>
          ; END_STREAM | END_HEADERS | PRIORITY
          (padlen=0, dep_stream_id=3, weight=32, exclusive=0)
          ; Open new stream
          :method: GET
          :path: /_next/static/chunks/472-b67f79dbdd2c1fe1.js
          :scheme: https
          :authority: streaming-frame-test.vercel.app
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.58.0
[  1.275] send HEADERS frame <length=48, flags=0x25, stream_id=25>
          ; END_STREAM | END_HEADERS | PRIORITY
          (padlen=0, dep_stream_id=3, weight=32, exclusive=0)
          ; Open new stream
          :method: GET
          :path: /_next/static/chunks/main-app-892c3dff08e9cd4c.js
          :scheme: https
          :authority: streaming-frame-test.vercel.app
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.58.0
[  1.275] send HEADERS frame <length=49, flags=0x25, stream_id=27>
          ; END_STREAM | END_HEADERS | PRIORITY
          (padlen=0, dep_stream_id=3, weight=32, exclusive=0)
          ; Open new stream
          :method: GET
          :path: /_next/static/chunks/polyfills-c67a75d1b6f99dc8.js
          :scheme: https
          :authority: streaming-frame-test.vercel.app
          accept: */*
          accept-encoding: gzip, deflate
          user-agent: nghttp2/1.58.0

recv DATA (resources)

リソースのリクエストを行いましたので、リソースの取得が行われます。
例えば、stream_id=15は下記のようになります。

そのフレームがストリームの最後の通信の場合、
END_STREAMフラグが設定されます。

[  1.288] recv (stream_id=15) :status: 200
[  1.288] recv (stream_id=15) accept-ranges: bytes
[  1.288] recv (stream_id=15) access-control-allow-origin: *
[  1.299] recv (stream_id=15) age: 172821
[  1.299] recv (stream_id=15) cache-control: public,max-age=31536000,immutable
[  1.299] recv (stream_id=15) content-disposition: inline; filename="c9a5bc6a7c948fb0-s.p.woff2"
[  1.299] recv (stream_id=15) content-type: font/woff2
[  1.299] recv (stream_id=15) date: Tue, 05 Dec 2023 19:53:10 GMT
[  1.299] recv (stream_id=15) etag: "74c3556b9dad12fb76f84af53ba69410"
[  1.299] recv (stream_id=15) server: Vercel
[  1.299] recv (stream_id=15) strict-transport-security: max-age=63072000; includeSubDomains; preload
[  1.299] recv (stream_id=15) x-matched-path: /_next/static/media/c9a5bc6a7c948fb0-s.p.woff2
[  1.299] recv (stream_id=15) x-vercel-cache: HIT
[  1.299] recv (stream_id=15) x-vercel-id: hnd1::frxb8-1701805990152-bbfbb030acf6
[  1.299] recv (stream_id=15) content-length: 46552
[  1.299] recv HEADERS frame <length=156, flags=0x04, stream_id=15>
          ; END_HEADERS
          (padlen=0)
          ; First response header
[  1.299] recv DATA frame <length=3513, flags=0x00, stream_id=15>
[  1.299] recv DATA frame <length=16384, flags=0x00, stream_id=15>
[  1.299] recv DATA frame <length=16384, flags=0x00, stream_id=15>
[  1.299] recv DATA frame <length=10271, flags=0x00, stream_id=15>
[  1.299] recv DATA frame <length=0, flags=0x01, stream_id=15>
          ; END_STREAM

recv DATA (HTML)

しばらくして、 HTML(stream_id=13)のDATA frameが受け取れます。
最後にEND_STREAMが設定され、ここでHTMLのストリームが閉じていることがわかります。

[  1.345] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
          (window_size_increment=38027)
[  2.476] recv DATA frame <length=28, flags=0x00, stream_id=13>
[  2.476] recv DATA frame <length=11, flags=0x00, stream_id=13>
[  2.493] recv DATA frame <length=327, flags=0x00, stream_id=13>
[  2.493] recv DATA frame <length=17, flags=0x00, stream_id=13>
[  2.493] recv DATA frame <length=10, flags=0x00, stream_id=13>
[  2.493] recv DATA frame <length=0, flags=0x01, stream_id=13>
          ; END_STREAM

送信されているHTMLを-vaオプションで実行し見てみると、
scriptを挿入することで、Streamingを行えるようにしているのがわかります。

[  0.538] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
          (window_size_increment=39670)
<script>self.__next_f.push([1,"b:[\"$\",\"div\",null,{\"children\":\"ok!hogehoge\"}]\n"])</script>[  0.968] recv DATA frame <length=28, flags=0x00, stream_id=13>
<script>self.__next_f.push([1,""])</script>[  0.968] recv DATA frame <length=11, flags=0x00, stream_id=13>
<div hidden id="S:0"><div>ok!hogehoge</div></div><script>$RC=function(b,c,e){c=document.getElementById(c);c.parentNode.removeChild(c);var a=document.getElementById(b);if(a){b=a.previousSibling;if(e)b.data="$!",a.setAttribute("data-dgst",e);else{e=b.parentNode;a=b.nextSibling;var f=0;do{if(a&&8===a.nodeType){var d=a.data;if("/$"===d)if(0===f)break;else f--;else"$"!==d&&"$?"!==d&&"$!"!==d||f++}d=a.nextSibling;e.removeChild(a);a=d}while(a);for(;c.firstChild;)e.insertBefore(c.firstChild,a);b.data="$"}b._reactRetry&&b._reactRetry()}};$RC("B:0","S:0")</script>[  0.969] recv DATA frame <length=327, flags=0x00, stream_id=13>
</body></html>[  0.969] recv DATA frame <length=17, flags=0x00, stream_id=13>
[  0.970] recv DATA frame <length=10, flags=0x00, stream_id=13>
[  0.970] recv DATA frame <length=0, flags=0x01, stream_id=13>
          ; END_STREAM

まとめ

streamingをHTTP/2フレーム単位で見ると、
HTMLのHEADERフレームを送信し、Open new streamします。
その後HTMLのDATAフレームの一部を先に受け取り、リソースの先読みが可能な場合は、
resourceリクエストHEADERを送信します。
Suspense内部の処理が準備完了すると<script>self.__next_f.push(.*)</script>の形で、
DATAフレームを順次受信し、全てが完了した後で、END_STREAMを行い、ストリームを閉じます。

HTMLのレスポンスヘッダーについて

https://streaming-frame-test.vercel.app/

次に、suspense内部でErrorを発生させるページ(/error/[ms])と、
suspenseを利用せずにErrorを発生させるページ(/error-sync/[ms])で、ステータスコードがどうなるかを見ると、suspense内部に例外を投げる処理を実装しても、ステータスコードとして500を返してくれません。

`/error/[ms]`code

import { Suspense } from "react";
async function SuspenseComponent ({ms}: {ms: string}) {
const fetched = await fetch(https://streaming-frame-test.vercel.app/api/?ms=${ms}&is_error=true)
if (fetched.ok) {
const obj = await fetched.json();
return <div>{obj.response}</div>
} else {
if ("true" === "true") { // 自明なtrue
console.log('error発生')
throw Error("エラー実行")
}
return <div>ng</div>
}
}

export default function Home({params}: {params: {ms: string}}) {
const ms = params.ms;
return (
<main>
<div>
Streaming-test
<Suspense fallback={<p>Loading...</p>}>
<SuspenseComponent ms={ms}/>
</Suspense>
</div>
</main>
)
}

export const dynamic = 'force-dynamic'

`/error-sync/[ms]`code

function SuspenseComponent () {
if ("true" === "true") { // 自明なtrue
throw "エラー実行"
}
return <div>error</div>
}

export default function Home() {
return (
<main>
<div>
error-test
{/* <Suspense fallback={<p>Loading...</p>}> /}
<SuspenseComponent />
{/
</Suspense> */}
</div>
</main>
)
}

export const dynamic = 'force-dynamic'


エラーをnghttp2でみる

/error/[ms]のステータスコードがどうなるかを見ます。
ステータスコードはHEADERフレームに紐づきますが、下記の実行結果を見ていただくと
/streaming/[ms]同様最初にHEADERフレームを受信し、DATAフレームによってSTREAMを終了し、
それ以降に、HEADERフレームを受信していません。

つまり、リクエストヘッダー情報は、Suspenseでwrapされてないコンポーネントのみで計算されレスポンスされます。

 nghttp -nva https://streaming-frame-test.vercel.app/error/500
 
 #...省略
 
 [  0.282] recv (stream_id=13) :status: 200
[  0.282] recv (stream_id=13) age: 0
[  0.282] recv (stream_id=13) cache-control: private, no-cache, no-store, max-age=0, must-revalidate
[  0.282] recv (stream_id=13) content-encoding: gzip
[  0.282] recv (stream_id=13) content-type: text/html; charset=utf-8
[  0.282] recv (stream_id=13) date: Thu, 07 Dec 2023 20:30:19 GMT
[  0.282] recv (stream_id=13) server: Vercel
[  0.282] recv (stream_id=13) strict-transport-security: max-age=63072000; includeSubDomains; preload
[  0.282] recv (stream_id=13) vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url
[  0.282] recv (stream_id=13) x-matched-path: /error/[ms]
[  0.282] recv (stream_id=13) x-powered-by: Next.js
[  0.282] recv (stream_id=13) x-vercel-cache: MISS
[  0.282] recv (stream_id=13) x-vercel-execution-region: iad1
[  0.282] recv (stream_id=13) x-vercel-id: hnd1::iad1::6d8b7-1701981018995-57af4cfcf377
[  0.282] recv HEADERS frame <length=308, flags=0x04, stream_id=13>
          ; END_HEADERS
          (padlen=0)
          ; First response header
 
 #...省略
  
 [  0.371] send WINDOW_UPDATE frame <length=4, flags=0x00, stream_id=0>
          (window_size_increment=32890)
[  0.948] recv DATA frame <length=42, flags=0x00, stream_id=13>
[  0.948] recv DATA frame <length=11, flags=0x00, stream_id=13>
[  0.948] recv DATA frame <length=169, flags=0x00, stream_id=13>
[  0.948] recv DATA frame <length=17, flags=0x00, stream_id=13>
[  0.948] recv DATA frame <length=10, flags=0x00, stream_id=13>
[  0.948] recv DATA frame <length=0, flags=0x01, stream_id=13>
          ; END_STREAM
[  0.948] send GOAWAY frame <length=8, flags=0x00, stream_id=0>
          (last_stream_id=0, error_code=NO_ERROR(0x00), opaque_data(0)=[])

error-sync/[ms]の方は、例外処理も含めて最初に計算されるため
ステータスコードとして500を返します。

nghttp -nva https://streaming-frame-test.vercel.app/error-sync/500

# ...省略

[  0.386] recv (stream_id=13) :status: 500
[  0.386] recv (stream_id=13) age: 0
[  0.386] recv (stream_id=13) cache-control: private, no-cache, no-store, max-age=0, must-revalidate
[  0.386] recv (stream_id=13) content-type: text/html; charset=utf-8
[  0.386] recv (stream_id=13) date: Thu, 07 Dec 2023 20:35:15 GMT
[  0.386] recv (stream_id=13) server: Vercel
[  0.386] recv (stream_id=13) strict-transport-security: max-age=63072000; includeSubDomains; preload
[  0.386] recv (stream_id=13) vary: RSC, Next-Router-State-Tree, Next-Router-Prefetch, Next-Url
[  0.386] recv (stream_id=13) x-matched-path: /error-sync/[ms]
[  0.386] recv (stream_id=13) x-powered-by: Next.js
[  0.386] recv (stream_id=13) x-vercel-cache: MISS
[  0.387] recv (stream_id=13) x-vercel-execution-region: iad1
[  0.387] recv (stream_id=13) x-vercel-id: hnd1::iad1::bx25k-1701981315100-7ffe07148d20
[  0.387] recv HEADERS frame <length=307, flags=0x04, stream_id=13>
          ; END_HEADERS
          (padlen=0)
          ; First response header

Discussion