After a protocol is deployed, clients, servers, proxies, caches, historical data, and offline tasks do not upgrade simultaneously. Compatibility design ensures that these different versions coexist within clear boundaries, rather than simply adding a version number to headers or requiring parsers to "accept as much as possible."
Dimension
Relationship to Maintain
Failure Example
Byte Format
Ability to locate fields and parse correctly
New field length breaks old parser
Data Semantics
Consistency in units, defaults, and meaning
The same number changes from seconds to milliseconds
Behavior
Consistency in state machines, errors, and retry semantics
New service allows intermediate states that old clients cannot handle
Security
Identity, permissions, and integrity requirements are not weakened
Authorization constraints are silently omitted during fallback
Operations
Availability of mixed versions, storage, and rollback
New data is persisted, but old programs cannot re-read it
Clarify Who Generates Data and Who Reads It First
Using data producers/consumers as a reference, a new reader being able to read old writer data is typically called backward compatibility; an old reader being able to handle new writer data is typically called forward compatibility. Terminology references in different communities may vary, so design documents should explicitly state the version direction.
"New client with old server" involves two paths: the new client writes requests for the old server to read, and the old server writes responses for the new client to read. These two paths require different capabilities and cannot be replaced by a single client/server age table instead of a data direction matrix.
How Changing a Field's Unit Turns Wire Compatibility into a Business Error
Assume the original field timeout_seconds = 1 is a uint32, and the new requirement wants to support milliseconds. Renaming the field to timeout_ms might still result in successful byte parsing, but the original value 5 would change from five seconds to five milliseconds, causing a semantic break. Switching to uint64 would not fix the unit meaning.
A more controllable evolution is to keep the old field, introduce a new, explicit timeout_ms = 2, define the priority when both appear, and maintain a writable method that is usable before the old reader exits. If 0 means "no timeout set," you must distinguish between "not provided" and "explicitly provided 0"; you cannot rely solely on truthiness checks.
# Teaching model: field names represent parsed fields; explicit presence is expressed by key existence.
defread_timeout_ms(message):if'timeout_ms'inmessage:value=message['timeout_ms']elif'timeout_seconds'inmessage:value=message['timeout_seconds']*1000else:value=30_000iftype(value)isnotintornot0<=value<=3_600_000:raiseValueError('invalid timeout')returnvalueassertread_timeout_ms({'timeout_seconds':5})==5000assertread_timeout_ms({'timeout_ms':0})==0assertread_timeout_ms({})==30000assertread_timeout_ms({'timeout_seconds':5,'timeout_ms':1200})==1200# If the old reader only supports whole seconds, 1.2s cannot be losslessly written back to the old field.
assert1200%1000!=0print('Units, defaults, explicit 0, and dual-field priority passed')
You cannot claim that any value for the new field can "automatically degrade." 1.2 seconds can only be rejected, have new features limited, or use an explicit lossy conversion strategy in the old format; the choice belongs to the business contract. In Protobuf, you can use the corresponding presence mechanism to express absence and explicit defaults; language-generated code and field versions must also be checked together. Protobuf Field Presence
Change
Checks Required
Add field
Whether old readers allow unknown values; whether new reader defaults are correct
Delete field
Keep field number/name; check historical data and old writers
Modify numeric type
Wire compatibility, range overflow, and language mapping changes
Add enum value
Default branch in old code, unknown value preservation, and business handling
Add oneof / change field ownership
Presence, mutual exclusion, and old value clearing semantics
Change mandatory constraints
Whether old writers can still generate accepted requests
After deleting a field in Protobuf, you should reserve the corresponding identifier to prevent old bytes from being interpreted as another meaning in the future. Having the same field number and readable wire type does not equal type, numeric, or business compatibility. Proto3 Updating Messages
Unknown Data: Skip, Preserve, or Reject Are Different Strategies
Scenario
Basis for Reasonable Behavior
Optional attributes affecting only display
Can be ignored per contract, preserving existing behavior
Transparent forwarding or persistence by middle layers
May need to preserve unknown fields to avoid backend losing new information
New permission constraints or commands that must be understood
Reject or disable features if not understood
Unknown frame without a universal length
Cannot blindly skip if boundaries cannot be located
Known message appearing in an error state
Should error according to the state machine, not pretend to be an optional extension
HTTP/2 defines rules for ignoring unknown frame types and specifies handling for unknown SETTINGS parameters, but still requires adherence to connection prefaces, CONTINUATION sequences, etc. QUIC v1 requires FRAME_ENCODING_ERROR for unknown transport frame types; it is not TLV with a uniform length per frame, so HTTP/2 rules cannot be simply copied over. HTTP/2, QUIC §12.4
HTTP/3 has its own frame and stream extension rules, requiring a distinction between "unknown" and "known but appearing in a prohibited location." Extension tolerance should be determined jointly by the syntax and semantics of each layer. HTTP/3 Extensions
Even if an old middle layer successfully parses and re-writes, it may lose information: converting Protobuf to JSON, copying fields one by one into DTOs, or storing in a database with only old columns all alter the unknown field path. You should test the round-trip result of new messages passing through the actual middle layer, not just test directly between two latest version libraries.
Why does a successful old relay lose a new field?
Send ms1200: preserving yields1.2s, rebuilding yields old5s. With ms0, preservation means unlimited; removing the field triggers5s fallback. A truthiness check cannot implement this precedence.
Preparing the visual
Why does a successful old relay lose a new field?
Skippable does not mean lossless forwarding. Explicit0 differs from absence; rebuilding known fields loses semantics even when parsing succeeds.
{"id":"unknown-field-roundtrip","title":"Why does a successful old relay lose a new field?","summary":"Skippable does not mean lossless forwarding. Explicit0 differs from absence; rebuilding known fields loses semantics even when parsing succeeds.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><div class=\"presets\" role=\"group\" data-i18n-label=\"mode\"><button data-mode=\"preserve\" data-i18n=\"preserve\"></button><button data-mode=\"rebuild\" data-i18n=\"rebuild\"></button></div><label class=\"check\"><input type=\"checkbox\" id=\"present\" checked><span data-i18n=\"present\"></span></label><div class=\"control\"><label for=\"value\" data-i18n=\"value\"></label><output for=\"value\" id=\"value-value\"></output><input id=\"value\" type=\"range\" min=\"0\" max=\"6000\" value=\"1200\" step=\"100\"></div><section class=\"panel\"><h3 data-i18n=\"path\"></h3><div id=\"path\"></div></section><div class=\"metrics\"><div><span data-i18n=\"result\"></span><strong id=\"result\"></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#path .tr{grid-template-columns:1fr 1.8fr}.tr>*{min-height:5.4em;font-family:var(--viz-font-ui)}","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 mode='preserve';function draw(){const original={seconds:5};if($('#present').checked)original.ms=+$('#value').value;const forwarded=mode==='preserve'?{...original}:{seconds:original.seconds},value=Object.hasOwn(forwarded,'ms')?forwarded.ms:forwarded.seconds*1000;set('value-value',$('#value').value);table('path',[['writer',JSON.stringify(original)],['old relay',JSON.stringify(forwarded)],['new reader',value+' ms']]);set('result',value);$('#result').dataset.value=value;pressed(mode);}document.querySelectorAll('input').forEach(e=>e.oninput=draw);document.querySelectorAll('[data-mode]').forEach(b=>b.onclick=()=>{mode=b.dataset.mode;draw()});draw();","audio":false,"strings":{"heading":"Why does a successful old relay lose a new field?","intro":"Choose unknown-field preservation and new-field presence/value; inspect final semantics at a new reader.","mode":"Old relay forwarding","path":"Object roundtrip path","present":"Include new timeout_ms","preserve":"Preserve unknown fields","rebuild":"Rebuild known fields only","reset":"Reset","result":"Timeout read by new reader (ms)","scope":"Teaching object: old seconds5, new ms takes precedence; absent ms falls back to seconds, all absent defaults30000. Relay knows seconds only; modes model path behavior, not every runtime.","scopeLabel":"Model scope and assumptions","value":"New value (ms;0 means unlimited)"}}
Negotiation, Discovery, and Fallback Should Not Be Mixed into One Step
Mechanism
Timing
Decision Content
HTTP/1.1 Upgrade
Protocol switch request/response within an existing connection
Switch on that connection if conditions are met
TLS ALPN
Within TLS handshake
Select the application protocol for this connection from the client-provided list
HTTP Alt-Svc
After receiving alternative service advertisement
Choose another connection or an existing alternative connection for subsequent requests
QUIC Version Mechanism
Connection establishment and applicable version negotiation process
Transport version and corresponding compatibility rules
Application Capability
Timing defined by the application protocol
Whether an optional feature is enabled by both parties
A typical modern use of HTTP Upgrade can be referenced in WebSocket; the old h2c Upgrade mechanism for HTTP/2 has been deprecated in RFC 9113 and should not continue to be used as a general tutorial for current HTTP/2 connection establishment. ALPN server selection in TLS 1.2 occurs in ServerHello, while in TLS 1.3 it occurs in EncryptedExtensions; you cannot draw a single ServerHello answer for all TLS versions. HTTP Upgrade, ALPN, TLS 1.3 Extension Locations
Alt-Svc does not convert an existing TCP connection into QUIC in place, nor does it force the client to migrate; the client still needs to verify the authority of the alternative service for the original origin. Advertisement validity, failure caching, and existing connections all affect actual selection. Alt-Svc
Version numbers and capabilities are not the same thing: some implementations of version 2 may lack a certain optional compression feature. The existence of a feature may have directionality, parameter ranges, and dependencies; a bitmap can only express a simple set of applicable capabilities, and the TLS cipher suites list itself is not a capability bitmap.
Find the Available Intersection First, Then Execute Local Policies
defchoose(local_preference,peer_offered,forbidden=()):offered=set(peer_offered)fornameinlocal_preference:ifnameinofferedandnamenotinforbidden:returnnameraiseValueError('no acceptable common mode')assertchoose(['v2','v1'],['v1'])=='v1'assertchoose(['v2','v1'],['v1','v2'])=='v2'try:choose(['v2','v1'],['v1'],forbidden={'v1'})exceptValueError:passelse:raiseAssertionError('local minimum policy bypassed')print('Intersection, preference, and local minimum requirements take effect separately')
This is a teaching selection function and does not represent that all protocols sort by the same party's preference. In practice, you must also verify that the peer's selection comes from a legitimate offer and bind the negotiation content that needs protection to the authentication handshake, preventing middle devices from deleting strong capabilities and forcing fallback.
Fallback is a strategy after a trial fails, not negotiation itself. Temporary network failures, version unsupported, certificate verification failure, and insufficient authorization should not all trigger the same fallback; fallback cannot silently cancel authentication or change business permissions. When there are minimum requirements, having no acceptable intersection is a failure.
GREASE: Continuously Use "Should Be Ignorable" Extension Locations
Protocol specifications say to ignore unknown values, but if the ecosystem only sends a few fixed values for a long time, buggy implementations might hardcode these values. GREASE maintains the usability of extension points by continuously sending reserved values in allowed locations, exposing such bugs early.
TLS GREASE uses values reserved by the specification, not arbitrary random bytes injected into arbitrary fields. The server should treat them as unknown candidates and select other legal capabilities; if the server returns GREASE values as real negotiation results, the client must reject them. It tests correct unknown value behavior, not requiring all locations to accept loosely. RFC 8701
HTTP/3 reserves values in the form of 0x1f * N + 0x21 for some settings and other spaces to exercise the corresponding ignore rules; different registries have their own regulations, and this formula should not be treated as a unified rule for TLS or all QUIC layers. GREASE also does not guarantee that every implementation sends all variants. HTTP/3 Reserved Settings
If an implementation only whitelists GREASE values for special handling and still errors on truly unknown values, it has not achieved the goal. Tests should cover different values, locations, lengths, and mixed known fields, while strictly rejecting inputs prohibited by the specification.
QUIC's Evolvability Has Clear Boundaries
QUIC encrypts and protects many transmission details, reducing the chance of middle devices making incorrect inferences about them; but it is not "all flags are encrypted, and middle boxes only see the CID." Long headers still have visible fields like version and connection ID, and some leading bit positions also have public meanings; header protection is also different from hiding the entire header content. QUIC Invariants
The initial protection key for QUIC Initial can be derived from public information and cannot serve as a universal guarantee to hide all handshake content from passive observers. Connection migration relies on CID, path validation, and state, not on completely removing IP/port from the network path. Evolvable internal formats and stable external invariants need to be designed together.
The base Version Negotiation packet itself lacks the authentication guarantees of a normal handshake; you cannot blindly trust the list within it to achieve secure fallback. RFC 9368 defines compatible version negotiation and related verification, and upgrade windows and service cluster capabilities also affect fallback prevention boundaries. QUIC Compatible Version Negotiation
Release Order Must Include Storage and Rollback
Assume the service first upgrades to dual-read but old-write, and old consumers still work normally; then confirm that all necessary paths can read the new fields, and gradually enable new writing. To support rollback to the original old binary, you cannot first write data that it cannot read or correctly interpret. A more common rollback target is a version that "supports dual-read but has new write features disabled."
Test Combination
What to Verify
Old writer → New reader
Historical data and defaults
New writer → Old reader
Unknown fields, ranges, and explicit failures for unsupported features
New → Old middle layer → New
Whether unknown information is preserved
New data → Rollback version
Boundaries between data and code rollback
Partial nodes have new capabilities
Whether routing, retries, and capability caching are correct
Truncation, duplication, unknown critical values
Whether failure behavior is consistent across different versions
Also account for the longest survival time of long connections, offline clients, retry queues, backups, and batch processing. "Production traffic no longer has old versions" does not mean old format data has completely disappeared. There should be usage evidence and a clear retention window before deleting old paths.
Lenient parsing without unified rules causes implementations to produce different results for the same input, creating interoperability and security issues. Clearly writing acceptable inputs, extension locations, and failure behaviors, and continuously verifying through mixed versions and actual data paths, allows the protocol to evolve stably. Maintaining Robust Protocols