HTTP/3 将 HTTP 语义映射到 QUIC,使用 QUIC 提供的可靠流、流控、拥塞控制和 TLS 1.3 握手。它减少一个流的传输缺口对其他流有序交付的牵连,但不会消除所有跨请求依赖。
方法、状态码、缓存等继承的是 HTTP 通用语义;HTTP/2 的9 B帧头、流窗口更新、优先级树等并未原样搬过来。排查时应区分 HTTP 层、QUIC 流、QUIC 包与 UDP 数据报。
问题 关键边界
UDP 丢包是否丢失 HTTP 内容 QUIC 负责可靠流数据恢复
流彼此是否完全独立 有序交付独立,但拥塞、额度和上层依赖仍共享
QPACK 表更新没到怎么办 引用未到表项的字段块可能阻塞
HTTP/3 帧中 Stream ID 在哪里 所属 QUIC 流已提供关联,不在 HTTP 帧头重复编码
第一次访问必须先走 HTTP/2 吗 还可以通过 DNS HTTPS 等发现端点
切换网络或 0-RTT 是否保证更快 取决于路径、状态、策略与实际请求
流映射:请求、控制、压缩指令分工
HTTP/3 把不同职责放到不同 QUIC 流
请求流:客户端双向流 0、4、8…
每流一次请求与响应;HEADERS / DATA
控制流:每端一条单向流
流类型 0x00;先 SETTINGS,再其他适用控制帧
QPACK 流:编码器、解码器分别单向
流类型 0x02 / 0x03;表更新与确认指令
QUIC Stream ID 与 HTTP/3 单向流类型是不同字段;类型 0x00 不意味着 QUIC 流号为 0。
请求使用客户端发起的双向 QUIC 流,ID 为0、4、8等;请求与响应分别占该流的两个方向。与 HTTP/2 不同,QUIC 流0可以承载业务请求,它不是连接控制帧专用流。
单向流开头先写 stream type,再承载该类型规定的内容。每端的控制流以 SETTINGS 为第一帧,同一控制流不能再次发送 SETTINGS;不存在 HTTP/2 那样的 SETTINGS ACK。QPACK 编码器和解码器流承载各自的指令格式,不是包在 HTTP HEADERS 或 DATA 帧里的表更新。
双方需允许对端建立至少三条单向流,为控制和 QPACK 留出空间及流控预算。已建立的关键流若意外关闭,会导致连接错误,不能把它当作完成一条普通请求后自然关闭。RFC 9114:流映射
帧头是变长整数,不再固定 9 B
HTTP/3 帧包含 Type、Length 两个 QUIC 变长整数以及 payload。长度只描述 payload,流ID由外层 QUIC 流提供。一般帧整数可以采用协议允许的编码长度,本例选最短编码。
取值范围 编码字节 最高两位
0 到 63 1 00
64 到 16,383 2 01
16,384 到 1,073,741,823 4 10
再到 2^62−1 8 11
5 B DATA 的 HTTP/3 帧为 Type 1 B + Length 1 B +内容5 B,共7 B。1000 B DATA 则是1+2+1000=1003 B;这些数字不包含 QUIC STREAM 帧、包保护、UDP/IP 头,不能直接拿来声称端到端节省了固定比例。
def varint ( value ) :
if not 0 <= value < 2 * * 62 :
raise ValueError ( ' out of range' )
for size , prefix in [ ( 1 , 0 ) , ( 2 , 1 ) , ( 4 , 2 ) , ( 8 , 3 ) ] :
if value < 2 * * ( 8 * size - 2 ) :
return ( value | ( prefix < < ( 8 * size - 2 ) ) ) . to_bytes ( size , ' big' )
def decode ( data ) :
size = 1 < < ( data [ 0 ] > > 6 )
if len ( data ) < size :
raise ValueError ( ' truncated' )
return int . from_bytes ( data [ : size ] , ' big' ) & ( ( 1 < < ( 8 * size - 2 ) ) - 1 ) , size
for n in [ 0 , 63 , 64 , 16383 , 16384 , 2 * * 30 - 1 , 2 * * 30 , 2 * * 62 - 1 ] :
value , used = decode ( varint ( n ) )
assert value == n and used == len ( varint ( n ) )
assert varint ( 64 ) . hex ( ) == ' 4040'
small = varint ( 0 ) + varint ( 5 ) + b ' hello'
large = varint ( 0 ) + varint ( 1000 ) + b ' x' * 1000
assert small . hex ( ) == ' 000568656c6c6f' and len ( small ) == 7
assert len ( large ) == 1003
try :
decode ( bytes ( [ 0x 40] ) )
except ValueError :
pass
else :
raise AssertionError ( ' accepted truncated integer' )
print ( ' HTTP/3 DATA 完整帧字节:' , len ( small ) , len ( large ) )
请求或响应使用 HEADERS、可选 DATA 和 trailer HEADERS,结束方向由 QUIC 流结束表达,不存在 HTTP/2 的 END_STREAM 标志。HEADERS 帧可跨多个 QUIC STREAM 帧或包,不需要 CONTINUATION;包边界不是字段块边界。
少了 TCP 队头阻塞,仍需面对共享资源
假设流0缺少一段内容,而流4完整数据已到,QUIC 可以继续交付流4;它不需要等待流0的缺口。但同一个丢失包可能装有多个流的数据,且丢包引起的拥塞窗口调整可能影响整个连接发送。
连接级 MAX_DATA、单流 MAX_STREAM_DATA、可开流数量、CPU和服务端排队也可能限制进度。HTTP/3 将相应流控交给 QUIC,不再发送 HTTP/2 WINDOW_UPDATE;与 HTTP/2 只对 DATA payload 计费不同,QUIC 流额度覆盖该流的字节,包括 HTTP 帧头和 HEADERS 数据。
因此“某一请求不因另一流字节缺口而被迫等待”是具体保证;“请求间完全独立”则过度扩大了它。QUIC 流与流控
QPACK:在压缩收益与阻塞风险之间选择
QPACK 允许选择依赖,也允许依赖导致有限阻塞
编码器流:插入第 6、7 项
途中延迟;接收端暂时只插入 5 项
请求 A:Required Insert Count=7
HEADERS 已到,但暂时不能解码
请求 B:无动态表引用
Required Insert Count=0
表更新到达后
累计插入数达到 7,A 可继续
B 可独立解码不代表完全没有共享影响:连接拥塞、流控、CPU 与应用资源仍共享。
HPACK 依赖有序连接中的压缩状态演进;HTTP/3 流可独立交付,所以 QPACK 将表更新和确认分离到专用流。字段块仍可能引用尚未收到的动态项,并通过 Required Insert Count 表达解码依赖。
图中解码器累计只插入5项,请求A要求7项,就需等待更新;请求B只用静态项或字面量,Required Insert Count=0,可以解码。这里是已经还原的插入计数,线上字段还涉及规定的计数编码和 Base/相对索引,不能把简单整数比较当完整解码器。
QPACK 静态表有99项,索引和 HPACK 不通用。动态表仍按连接的编码方向分别维护;解码器返回 Section Acknowledgment、Stream Cancellation、Insert Count Increment 等信息,帮助编码器跟踪依赖和可释放状态。这些指令的单位和释放条件不同,不能统称为一次“表同步”。
SETTINGS_QPACK_MAX_TABLE_CAPACITY 与 SETTINGS_QPACK_BLOCKED_STREAMS 默认都为0。允许的阻塞流数不是“所有正在请求的流数”;编码器可选已知安全的引用或字面量,以更低压缩率避免增加潜在阻塞。Never Indexed 也不等于加密字段。RFC 9204:QPACK
流字节已齐,为什么字段仍不能解码?
先递送A的全部HEADERS,它仍等待第6、7次插入;递送B却立即可解码。补到6时A仍不满足依赖,补到7才解锁。这里的等待发生在QPACK依赖上,不能归因于A的流字节缺口。
正在呈现知识画面 重新加载
流字节已齐,为什么字段仍不能解码? 可靠流交付完成不等于字段解码完成。只有引用动态项的块等待相应插入,静态/字面量块可以先完成。
{"id":"qpack-required-inserts","title":"流字节已齐,为什么字段仍不能解码?","summary":"可靠流交付完成不等于字段解码完成。只有引用动态项的块等待相应插入,静态/字面量块可以先完成。","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"actions\"><button id=\"a\" data-i18n=\"a\"></button><button id=\"b\" data-i18n=\"b\"></button><button id=\"insert\" data-i18n=\"insert\"></button><button id=\"reset\" data-i18n=\"reset\"></button></div><section class=\"panel\"><h3 data-i18n=\"states\"></h3><div id=\"states\"></div></section><div class=\"metrics\"><div><span data-i18n=\"count\"></span><strong id=\"count\"></strong></div><div><span data-i18n=\"blocked\"></span><strong id=\"blocked\"></strong></div></div><details class=\"scope\"><summary data-i18n=\"scopeLabel\"></summary><p data-i18n=\"scope\"></p></details>","css":".intro{margin:8px 0 18px;color:var(--muted);font-size:14px}.caption,.hint{font-size:12px;color:var(--muted);margin-top:12px}.control{display:grid;grid-template-columns:minmax(0,1fr) auto;gap:6px 12px;align-items:center;margin:14px 0;font-size:13px}.control label{font-size:13px}.control output{color:var(--accent);min-width:5em;text-align:right;font-variant-numeric:tabular-nums}.control input{grid-column:1/-1;width:100%;margin:0}.presets{display:flex;gap:4px;align-items:stretch}.presets button{flex:1 1 0;min-width:0;overflow-wrap:anywhere;font-size:13px}.actions{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.actions button{flex:1;min-width:0;font-size:13px;min-height:42px}.copy{display:grid;font-size:14px;margin:16px 0}.copy>*{grid-area:1/1;margin:0}.reserve{visibility:hidden}.scope{margin-top:14px;color:var(--muted);font-size:12px}.scope summary{padding:8px 0;cursor:pointer}.scope p{margin-top:10px}.two{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.card{border:1px solid var(--rule);border-radius:9px;padding:14px;min-width:0}.card header{font-size:14px;font-weight:600;margin-bottom:10px}.card button{font-size:12px;width:100%;min-width:0}.card small{display:block;font-size:12px;color:var(--muted)}.card code{font-size:12px;overflow-wrap:anywhere}.metrics{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:16px 0}.metrics>div{border-left:2px solid var(--accent);padding-left:10px;min-width:0}.metrics span{display:block;min-height:3.2em;color:var(--muted);font-size:12px}.metrics strong{font-size:23px;display:block;font-weight:500;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.formula{border-top:1px solid var(--rule);padding-top:12px;margin-top:14px;font-size:13px;font-variant-numeric:tabular-nums;overflow-wrap:anywhere}.value{font:500 20px var(--viz-font-ui)}.legend{display:flex;flex-wrap:wrap;gap:6px 14px;font-size:12px;color:var(--muted);margin:12px 0}.legend span:before{content:'';display:inline-block;width:8px;height:8px;border-radius:2px;background:var(--accent);margin-right:6px}.legend .second:before{background:var(--second)}.check{display:flex;align-items:center;gap:8px;font-size:13px;margin:12px 0}.check input{width:16px;height:16px;flex-shrink:0;accent-color:var(--accent)}select,input[type=text],input[type=number]{background:var(--paper);color:var(--ink);font:inherit;font-size:13px;padding:10px;border:1px solid var(--rule);border-radius:7px;max-width:100%;min-width:0}select{width:100%;min-height:42px}.band{height:20px;display:flex;background:var(--surface);border-radius:4px;overflow:hidden}.band i{height:100%;display:block;background:var(--accent);transition:width .25s}.band i.second{background:var(--second)}button:disabled{cursor:default}.status{font-size:13px;min-height:4.8em}.mono{font-family:var(--viz-font-mono);font-variant-numeric:tabular-nums}.muted{color:var(--muted)}\n@media(max-width:480px){.two{gap:8px}.card{padding:12px 9px}.actions button,.presets button{font-size:12px}.metrics strong{font-size:20px}.metrics{gap:8px}}\n@media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important;animation:none!important}}\n.track-row{margin:16px 0}.track-row>span{display:block;font-size:12px;color:var(--muted);margin-bottom:7px}.track{display:flex;gap:5px;min-width:0}.cell{flex:1;min-width:0;min-height:48px;border:1px solid var(--rule);border-radius:5px;background:var(--surface);display:flex;align-items:center;justify-content:center;text-align:center;padding:5px 3px;font-size:12px;overflow-wrap:anywhere;font-variant-numeric:tabular-nums}.cell.on{background:var(--accent-soft);border-color:var(--accent)}.cell.gold{background:var(--second-soft);border-color:var(--second)}.cell.empty{border-style:dashed;color:var(--muted)}.cell.error{border-color:var(--second);text-decoration:line-through}.choices{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px;margin:14px 0}.choices button{min-width:0;padding:10px 4px;font-size:12px;overflow-wrap:anywhere}.choices button[aria-current=true]{border-color:var(--accent);background:var(--accent-soft)}.relation{display:grid;grid-template-columns:minmax(0,1fr) 28px minmax(0,1fr);align-items:center;gap:6px;margin:12px 0}.relation .cell{min-height:72px}.relation>i{text-align:center;font-style:normal;color:var(--accent)}.readout{font-size:13px;white-space:pre-line;min-height:6.4em;overflow-wrap:anywhere}.plot{position:relative;border-left:1px solid var(--rule);border-bottom:1px solid var(--rule);padding:8px 0;margin:18px 0}.plot-row{height:28px;position:relative;margin:4px 0;background:var(--surface)}.plot-row i{position:absolute;inset-block:5px;background:var(--accent);min-width:2px;transition:width .2s,left .2s}.plot-row b{position:relative;padding:4px 6px;font:11px/28px var(--viz-font-ui);background:var(--paper);border-radius:3px}.axis{display:flex;justify-content:space-between;font-size:11px;color:var(--muted)}\n\n/* Action labels must remain readable across locales and narrow embeds. */\n.actions{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:8px}\n.actions button{overflow-wrap:anywhere;white-space:normal;min-height:58px;padding:8px}\nbutton{overflow-wrap:anywhere}\n@media(min-width:640px){.actions{grid-template-columns:repeat(3,minmax(0,1fr))}}\n\n.metrics span{overflow-wrap:anywhere}\n.panel{border:1px solid var(--rule);border-radius:9px;padding:12px;margin:14px 0;min-width:0}.panel h3{font-size:13px;margin:0 0 10px;color:var(--muted)}.source{font:13px/1.8 var(--viz-font-mono);white-space:pre-wrap;overflow-wrap:anywhere;margin:12px 0}.source mark{background:var(--accent-soft);color:var(--ink);border-bottom:2px solid var(--accent)}.source .fault{background:var(--second-soft);border-color:var(--second)}.diagram{display:block;width:100%;height:auto;max-height:320px;margin:14px 0;overflow:visible}.diagram text{fill:var(--ink);font:12px var(--viz-font-ui)}.diagram .edge{stroke:var(--muted);stroke-width:1.5;fill:none}.diagram .edge.on{stroke:var(--accent);stroke-width:3}.diagram .node{fill:var(--surface);stroke:var(--rule);stroke-width:1.5}.diagram .node.on{fill:var(--accent-soft);stroke:var(--accent)}.diagram .node.gold{fill:var(--second-soft);stroke:var(--second)}.log{font:12px/1.8 var(--viz-font-mono);white-space:pre-line;overflow-wrap:anywhere;min-height:7.2em}.table{display:grid;gap:5px;margin:14px 0}.table .tr{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:5px}.table .tr>*{padding:8px 5px;background:var(--surface);border-radius:4px;min-width:0;overflow-wrap:anywhere;font-size:12px}.table .tr .on{background:var(--accent-soft)}.table .tr .gold{background:var(--second-soft)}.choices button{min-height:44px}.metrics strong{min-height:1.6em}.code-line{display:block}.code-line.on{background:var(--accent-soft);border-left:2px solid var(--accent);padding-left:6px}.status-badge{font-size:12px;color:var(--accent)}.tile-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:6px}.tile-grid .cell{min-height:40px}.actions button{min-height:48px}.form-field{display:grid;gap:8px;margin:14px 0;font-size:13px}.form-field input{width:100%;font-family:var(--viz-font-ui)}.metrics.three{grid-template-columns:repeat(3,minmax(0,1fr))}.formula{min-height:3.2em}.presets{flex-wrap:wrap}.presets button{flex-basis:25%;min-height:44px}\n\n.budget{display:flex;height:28px;margin:16px 0;background:var(--surface);border-radius:5px;overflow:hidden}.budget i{display:block;background:var(--accent);transition:width .2s}.budget i.gold{background:var(--second)}.intervals{margin:16px 0}.interval{position:relative;height:36px;background:var(--surface);margin:6px 0;border-radius:4px;overflow:hidden}.interval i{position:absolute;height:100%;background:var(--accent-soft);border-left:2px solid var(--accent)}.interval i.gold{background:var(--second-soft);border-color:var(--second)}.interval span{position:relative;z-index:1;font:12px/36px var(--viz-font-ui);padding-left:7px}.table .tr>*{min-height:4.8em}.formula{white-space:pre-line;min-height:7.2em}.actions:has(>button:only-child){grid-template-columns:1fr}.metrics strong{min-height:3.2em}.rangegrid{display:grid;grid-template-columns:repeat(12,minmax(0,1fr));gap:3px;margin:14px 0}.rangegrid .cell{min-height:30px;padding:3px 1px;font-size:10px}.message{min-height:7.2em;white-space:pre-line;font:13px/1.8 var(--viz-font-ui);overflow-wrap:anywhere}\n/* Allow CJK hanging punctuation without overflowing a metric's border box. */\n.metrics>div{padding-right:4px}.metrics span{line-break:anywhere}\n\n#states .tr{grid-template-columns:.5fr .8fr 1.7fr}.tr>*{min-height:4.8em}","js":"const $=s=>document.querySelector(s);const set=(id,v)=>$('#'+id).textContent=v;const t=k=>viz.t(k);function cells(id,values){$('#'+id).replaceChildren(...values.map(v=>{const e=document.createElement('div');e.className='cell '+(v.cls||'');e.textContent=v.text;e.title=v.title||v.text;return e;}));}function pressed(mode){document.querySelectorAll('[data-mode]').forEach(e=>e.setAttribute('aria-pressed',String(e.dataset.mode===mode)));}const esc=s=>String(s).replace(/[&<>\"']/g,c=>({'&':'&','<':'<','>':'>','\"':'"',\"'\":'''}[c]));const fmt=s=>'{'+[...s].sort().join(', ')+'}';function graph(id,nodes,edges){const el=$('#'+id);el.setAttribute('viewBox','0 0 360 200');el.innerHTML='<defs><marker id=\"arrow-'+id+'\" viewBox=\"0 0 10 10\" refX=\"8\" refY=\"5\" markerWidth=\"5\" markerHeight=\"5\" orient=\"auto-start-reverse\"><path d=\"M 0 0 L 10 5 L 0 10 z\" fill=\"var(--muted)\"/></marker></defs>'+edges.map(e=>{const a=nodes[e[0]],b=nodes[e[1]],dx=b.x-a.x,dy=b.y-a.y,d=Math.hypot(dx,dy)||1;if(edges.some(r=>r[0]===e[1]&&r[1]===e[0])){const nx=-dy/d,ny=dx/d;return '<path class=\"edge '+(e[2]?'on':'')+'\" d=\"M '+(a.x+dx/d*22+nx*12)+' '+(a.y+dy/d*22+ny*12)+' Q '+((a.x+b.x)/2+nx*38)+' '+((a.y+b.y)/2+ny*38)+' '+(b.x-dx/d*25+nx*12)+' '+(b.y-dy/d*25+ny*12)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}return '<line class=\"edge '+(e[2]?'on':'')+'\" x1=\"'+(a.x+dx/d*25)+'\" y1=\"'+(a.y+dy/d*25)+'\" x2=\"'+(b.x-dx/d*28)+'\" y2=\"'+(b.y-dy/d*28)+'\" marker-end=\"url(#arrow-'+id+')\"/>';}).join('')+nodes.map(n=>'<g><circle class=\"node '+(n.cls||'')+'\" cx=\"'+n.x+'\" cy=\"'+n.y+'\" r=\"25\"/><text x=\"'+n.x+'\" y=\"'+(n.y+4)+'\" text-anchor=\"middle\">'+esc(n.name)+'</text>'+(n.sub?'<text x=\"'+n.x+'\" y=\"'+(n.y+43)+'\" text-anchor=\"middle\">'+esc(n.sub)+'</text>':'')+'</g>').join('');}function table(id,rows){$('#'+id).classList.add('table');$('#'+id).replaceChildren(...rows.map(r=>{const row=document.createElement('div');row.className='tr';r.forEach(x=>{const c=document.createElement('div');if(typeof x==='object'){c.textContent=x.text;c.className=x.cls||'';}else c.textContent=x;row.append(c)});return row;}));}function band(id,parts,total){$('#'+id).replaceChildren(...parts.map((p,i)=>{const e=document.createElement('i');e.style.width=(100*p.value/total)+'%';e.className=i%2?'gold':'on';e.title=p.name+': '+p.value;return e}));}function intervals(id,items,total){$('#'+id).replaceChildren(...items.map(p=>{const row=document.createElement('div');row.className='interval';const bar=document.createElement('i');bar.style.left=(100*p.start/total)+'%';bar.style.width=(100*(p.end-p.start)/total)+'%';bar.className=p.cls||'';const label=document.createElement('span');label.textContent=p.label;row.append(bar,label);return row;}));}let count=5,got={A:false,B:false};function draw(){const rows=[['stream','RIC','state']];for(const [id,ric] of [['A',7],['B',0]])rows.push([id,ric,!got[id]?'not received':count<ric?'BLOCKED':'decoded']);table('states',rows);set('count',count);set('blocked',Number(got.A&&count<7));$('#insert').disabled=count>=7;$('#states').dataset.snapshot=JSON.stringify({count,got});}$('#a').onclick=()=>{got.A=true;draw()};$('#b').onclick=()=>{got.B=true;draw()};$('#insert').onclick=()=>{count++;draw()};$('#reset').onclick=()=>{count=5;got={A:false,B:false};draw()};draw();","audio":false,"strings":{"a":"递送 A 的 HEADERS","b":"递送 B 的 HEADERS","blocked":"等待插入的流数","count":"已接收插入数","heading":"流字节已齐,为什么字段仍不能解码?","insert":"递送一条插入指令","intro":"独立递送两个请求的字段块和编码器插入指令,检查各流依赖。","reset":"重置","scope":"解码器初始插入计数5,A的已还原Required Insert Count7、B为0;允许1条阻塞流,不模拟计数线上编码、Base、淘汰和编码器ACK状态。","scopeLabel":"模型范围与约定","states":"各流状态"}}
发现端点:Alt-Svc 与 DNS HTTPS
Alt-Svc: h3=":443"; ma=86400 表示原服务提供一个同主机443端口的 HTTP/3 替代服务,max-age描述替代服务信息的缓存时间。它不是 HTTP 内容缓存时长,也不要求客户端立即把已有连接迁过去。
替代服务不改变原始 origin。即使连接到其他主机名或端口,认证仍需证明有权代表原服务;不能把一次位置提示当作自动信任新身份的授权。RFC 7838:替代服务
DNS HTTPS/SVCB 记录也可提供 alpn、port 等服务参数,帮助客户端在访问前发现 h3,不必每次先通过 h1/h2 收到 Alt-Svc。记录只是发现材料,客户端能力、证书验证及连接结果仍要满足要求。RFC 9460
UDP 被阻断、版本不兼容或路径不可达时,客户端可能竞速或回退到 TCP 上的 HTTP。具体等待时长由实现决定,不能承诺“尝试 QUIC 失败完全没有延迟”。指标应拆开首次发现、建连、回退和业务处理时间。
0-RTT 与迁移:依赖先前状态与路径验证
0-RTT 需要可用的恢复状态,并可能被服务器拒绝。发送早期请求时还要使用适当的已记忆 HTTP/3 设置和传输参数;拒绝后何时重发、是否可以重发都要遵守协议及业务约定。
早期数据具有重放风险。GET 名义上安全,不证明实际应用绝无副作用;扣款、提交订单等操作不能仅因延迟更低就放进early data。HTTP 的 Early-Data 标记和425 Too Early 可帮助中间件与源站协调,425后的重试不应再次放到early data中。RFC 8470
连接迁移可以在地址变化后保持连接身份,但要求新路径可用、验证及连接状态仍在,并受端点策略限制。Wi-Fi切换到蜂窝时可能经历探测、拥塞状态调整或失败,不能保证每次切换都无中断。服务端负载均衡也需把适当CID路由到持有连接状态的实例。QUIC 迁移
取消与 GOAWAY:边界和 HTTP/2 不同
QUIC 的 RESET_STREAM、STOP_SENDING 等参与请求终止,但“取消流”不等于业务事务回滚。没有响应时,需要判断请求是否可能已经被处理,再决定业务重试。
服务端 HTTP/3 GOAWAY 中的请求流ID是“不再接受”的边界:大于或等于它的请求不会被处理,小于它的请求可能已经处理。若边界为8,流8、12可在其他连接重试,流0、4则不能仅据此认定未执行。HTTP/2 的 Last-Stream-ID 是可能已处理的包含式上界,切勿复制成同一种比较运算。
多次GOAWAY的边界不能增加。客户端发出的GOAWAY使用推送ID,不能把它直接解释成同样的客户端请求流ID。RFC 9114:退出
推送、DATAGRAM 与观测
HTTP/3 基础规范仍定义推送和相应许可控制,浏览器或服务器产品是否支持另行核实;不能直接写成协议已废弃全部推送。普通 HTTP DATA 仍走可靠流,QUIC支持DATAGRAM不意味着所有HTTP/3响应自动变成不可靠传输。
抓包首先确认UDP上的QUIC、版本与连接建立,再结合可用的解密材料或实现事件追踪查看HTTP帧、QPACK阻塞和流控;仅凭UDP端口443不能判断请求成功。应用状态码与QUIC传输关闭错误也应分开记录,才能区分业务拒绝与连接故障。
继续查阅