gRPC describes RPC methods and message types using service definitions, while tonic provides the asynchronous implementation for Rust, commonly combined with Tokio, Tower, and Prost. Generated code reduces the need for hand-written serialization and interface boilerplate, but the two ends of the network will not automatically recompile just because one side modifies the .proto file.
There are three layers: compile-time type checking of the protocol, online encoding compatibility, and business semantics. HTTP APIs can also use descriptions and code generation tools like OpenAPI; the reason for choosing gRPC should come from the calling model, streaming capabilities, and ecosystem requirements, rather than dismissing other types of interfaces as lacking contracts.
Understand the Four Types of RPC First
Form
Number of Request Messages
Number of Response Messages
Typical Use Case
Unary
1
1
A single query or command
Server streaming
1
Multiple
Sending query results incrementally
Client streaming
Multiple
1
Uploading in batches and then aggregating
Bidirectional streaming
Multiple
Multiple
Independent progress in both directions
Bidirectional streaming does not require sending one response for every request received, nor does it guarantee a one-to-one correspondence between the two directions. Messages within each direction are ordered, but the application still needs to define message IDs, completion conditions, and recovery methods after failures.
The following uses the tonic 0.14 series. The Prost integration for this series is located in tonic-prost and tonic-prost-build; do not directly mix in the old tutorial's tonic_build::compile_protos. You need protoc installed locally, and save Cargo.lock to record the actual resolved versions. tonic, tonic-prost-build
Cargo.toml:
[package]
name = "grpc-demo"
version = "0.1.0"
edition = "2024"
[dependencies]
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tonic = "0.14"
tonic-prost = "0.14"
prost = "0.14"
[build-dependencies]
tonic-prost-build = "0.14"
Run cargo run --bin server and cargo run --bin client in two separate terminals; the client should print Hello, Rust!. Local plaintext listening here is for observing the request chain; real deployments should configure TLS, authentication, and resource limits according to the environment.
#[tonic::async_trait] is used to match the trait form generated by this series. Rust has supported native async fn in traits since 1.75; keeping the macro cannot be interpreted as the language "not yet supporting" it; native async traits, dynamic dispatch, and conventions used by generation libraries need to be viewed separately.
Why Modifying .proto Doesn't Necessarily Break Old Clients
Protobuf binary encoding relies on field numbers. When adding compatible fields, old clients may still successfully read fields they recognize; whether local Rust code needs modification depends on whether it is regenerated and how types are constructed. Online compatibility cannot be proven solely by a single repository compiling successfully.
After deleting a field, you should retain its number, and usually its name to prevent future misuse; do not reuse old numbers to express different meanings. Changes to field types must be judged item by item according to wire compatibility rules. Even if the encoding can still be decoded, changing the unit from seconds to milliseconds might directly break business semantics. Proto3 Update Rules
When distinguishing between "not provided" and "provided with a default value," you should explicitly use field presence design, such as applicable optional fields. An empty string does not automatically become a validation error just because the type is string, which is why the handler above explicitly checks name.
Verify Field Presence and Old Relay Nodes with Two Message Types
Below is an independent src/bin/compat.rs, using the project's existing Prost dependency, directly declaring messages to observe both old and new structures in the same program. Field number 1 retains name, and version 2 adds an optionallocale field with number 2.
useprost::Message;#[derive(Clone, PartialEq, Message)]structV1{
#[prost(string, tag = "1")]
name: String,
}#[derive(Clone, PartialEq, Message)]structV2{
#[prost(string, tag = "1")]
name: String,
#[prost(string, optional, tag = "2")]
locale:Option<String>,
}fnmain(){let absent =V2{ name:String::new(), locale:None};let empty =V2{ name:String::new(), locale:Some(String::new())};assert!(absent.encode_to_vec().is_empty());assert_eq!(empty.encode_to_vec(),[0x12,0]);let new =V2{ name:"Rust".into(), locale:Some("zh-CN".into())};let bytes = new.encode_to_vec();let old =V1::decode(bytes.as_slice()).unwrap();assert_eq!(old.name,"Rust");let relayed = old.encode_to_vec();let decoded =V2::decode(relayed.as_slice()).unwrap();assert_eq!(decoded.name,"Rust");assert_eq!(decoded.locale,None);}
optional maps missing and explicitly empty values to None and Some(""). In this example, 0x12 is the key for field number 2, wire type 2, and the following 0 indicates a zero-byte length; thus, even if the business strings are both empty, their online representations are different. Prost Field Representation
The old V1 can read the recognized name from the new data, but this Prost message structure will not retain the unknown locale when re-encoding. Thus, "old clients can read messages with new fields" and "new fields can still reach the destination after being decoded and re-encoded by old nodes" are two different compatibility requirements. If the old node merely passes through the raw message bytes, no lossy reconstruction occurs here; specific chains must be verified according to the actual conversion process.
Check Before Release by Read/Write Direction
Scenario
Result to Check
New Write → Old Read
Can unknown fields be skipped, and does the old business still work correctly?
Old Write → New Read
Do missing values have reasonable meanings? Don't assume all are valid by default.
New Write → Old Relay → New Read
Does re-encoding or JSON conversion lose new information?
New Enum Value → Old Logic
Is the original integer preserved? Is the default meaning incorrectly applied?
Data Compatible but Unit Changed
Have business calculations, ranges, and displays changed?
Gradual release usually lets the reader accept the new format first, then enables new writers; rollback also requires checking if already written new data can be reasonably handled by the old version. Retaining field numbers prevents encoding misreading, but does not automatically solve changes in units, permissions, or state machine semantics.
Can an old reader preserve a new field?
Compare two subtly different messages: None omits field 2, while Some("") sends its key and zero length. Raw forwarding preserves the distinction; the article’s old Prost V1 knows only name and loses locale when rebuilding. Compatibility must examine the whole read/write path, not just whether an old endpoint decodes.
Preparing the visual
Can an old reader preserve a new field?
Some("") adds field 2 as 12 00; old Prost decode/re-encode drops undeclared fields, while raw forwarding preserves them.
{"id":"rust-protobuf-presence","title":"Can an old reader preserve a new field?","summary":"Some(\"\") adds field 2 as 12 00; old Prost decode/re-encode drops undeclared fields, while raw forwarding preserves them.","height":1000,"html":"<h2 data-i18n=\"heading\"></h2><p class=\"intro\" data-i18n=\"intro\"></p><label class=\"check\"><input type=\"checkbox\" id=\"present\" checked><span data-i18n=\"present\"></span></label><div class=\"presets\" role=\"group\" data-i18n-label=\"mode\"><button data-mode=\"raw\" data-i18n=\"raw\"></button><button data-mode=\"decode\" data-i18n=\"decode\"></button></div><div class=\"track-row\"><span data-i18n=\"before\"></span><div id=\"before\" class=\"track\"></div></div><div class=\"track-row\"><span data-i18n=\"after\"></span><div id=\"after\" class=\"track\"></div></div><section class=\"panel\"><h3 data-i18n=\"receiver\"></h3><div id=\"receiver\"></div></section><div class=\"copy\"><p id=\"explanation\" role=\"status\"></p><p class=\"reserve\" aria-hidden=\"true\" data-i18n=\"explain\"></p></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:550;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:550 20px ui-monospace,monospace}.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:ui-monospace,monospace;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 ui-monospace,monospace;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 ui-monospace,monospace;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 ui-monospace,monospace}.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 ui-monospace,monospace;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:ui-monospace,monospace}.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 ui-monospace,monospace;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 ui-monospace,monospace;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.chart-wrap{margin:18px 0}.chart-label{font-size:12px;color:var(--muted);min-height:3.2em}.curve{display:block;width:100%;height:auto;max-height:320px;overflow:visible}.curve text{font:11px ui-monospace,monospace;fill:var(--muted)}.gridline{stroke:var(--rule);stroke-width:1}.metrics strong{font-size:20px;min-height:3.2em}.formula{font:12px/1.8 ui-monospace,monospace;min-height:9em}.readout{min-height:8em}.presets button{flex-basis:30%}.scope p{overflow-wrap:anywhere} .unit-note{font-size:12px;color:var(--muted);margin:10px 0}\n\n.metrics span{word-break:break-all;line-break:strict}.metrics>div{padding-right:6px}\n\n.cell{min-height:72px}.metrics strong{min-height:3.6em}\n\n#receiver{min-height:3em;font:16px ui-monospace,monospace}","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';row.style.gridTemplateColumns='repeat('+r.length+',minmax(0,1fr))';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;}));}function plot(id,series,xr,yr,opts={}){const e=$('#'+id),X=x=>42+(x-xr[0])/(xr[1]-xr[0])*298,Y=y=>175-(y-yr[0])/(yr[1]-yr[0])*148;let out='<defs><clipPath id=\"clip-'+id+'\"><rect x=\"42\" y=\"27\" width=\"298\" height=\"148\"/></clipPath></defs>';for(let i=0;i<3;i++){let x=xr[0]+i*(xr[1]-xr[0])/2,y=yr[0]+i*(yr[1]-yr[0])/2;out+='<path class=\"gridline\" d=\"M '+X(x)+' 27V175M42 '+Y(y)+'H340\"/><text x=\"'+X(x)+'\" y=\"194\" text-anchor=\"middle\">'+esc(opts.xfmt?opts.xfmt(x):Number(x.toFixed(2)))+'</text><text x=\"36\" y=\"'+(Y(y)+4)+'\" text-anchor=\"end\">'+esc(opts.yfmt?opts.yfmt(y):Number(y.toFixed(2)))+'</text>';}out+='<g clip-path=\"url(#clip-'+id+')\">';for(const s of series){out+='<path fill=\"none\" stroke=\"'+(s.color||'var(--accent)')+'\" stroke-width=\"2.5\" '+(s.dash?'stroke-dasharray=\"5 4\"':'')+' d=\"'+s.pts.map((p,i)=>(i?'L':'M')+X(p[0]).toFixed(2)+','+Y(p[1]).toFixed(2)).join(' ')+'\"/>';}for(const p of opts.points||[])out+='<circle cx=\"'+X(p[0])+'\" cy=\"'+Y(p[1])+'\" r=\"4\" fill=\"var(--second)\" stroke=\"var(--paper)\" stroke-width=\"1.5\"/>';if(opts.cursor!==undefined)out+='<path d=\"M'+X(opts.cursor)+' 27V175\" stroke=\"var(--muted)\" stroke-dasharray=\"3 3\"/>';e.innerHTML=out+'</g>';e.dataset.curves=JSON.stringify(series.map(s=>s.pts));}const curve=(fn,lo,hi,n=120)=>Array.from({length:n+1},(_,i)=>{const x=lo+(hi-lo)*i/n;return[x,fn(x)]});function inputs(draw){document.querySelectorAll('input,select').forEach(e=>{e.addEventListener('input',draw);e.addEventListener('change',draw)});draw()}const n=id=>+$('#'+id).value;const show=(id,v,d=3)=>set(id,Number(v.toFixed(d)));let mode='raw';function draw(){pressed(mode);let p=$('#present').checked,a=['0A','01','41',...(p?['12','00']:[])],b=mode==='decode'?a.slice(0,3):a;for(let [id,vals]of [['before',a],['after',b]])cells(id,Array.from({length:5},(_,i)=>({text:vals[i]||'—',cls:i<vals.length?(i>=3?'gold':'on'):'empty'})));set('receiver',p&&mode==='raw'?'Some(\"\")':'None');set('explanation',t('explain'));}document.querySelectorAll('[data-mode]').forEach(b=>b.onclick=()=>{mode=b.dataset.mode;draw()});inputs(draw);","audio":false,"strings":{"after":"Bytes after forwarding","before":"Bytes sent by V2","decode":"V1 decode/re-encode","explain":"12 = (2 << 3) OR 2; 00 is zero length. Encoded presence differs from nonempty text.","heading":"Can an old reader preserve a new field?","intro":"Choose absent or explicitly empty optional locale, then raw forwarding or old Prost re-encoding.","mode":"Old intermediary behavior","present":"Send explicit Some(\"\")","raw":"Forward raw bytes","receiver":"V2 receiver locale","reset":"Reset","scope":"Fixed name=\"A\" at field 1 and optional locale at field 2; models the article’s Prost V1/V2, not every Protobuf implementation.","scopeLabel":"Model scope and assumptions"}}
Status, Deadlines, and Retries
Status represents the RPC result; you cannot just look at the HTTP response status. Common classifications are as follows, see gRPC Status Codes for complete code definitions:
Code
Judgment in Application
InvalidArgument
The request itself does not meet requirements; retrying as-is is usually futile
Unauthenticated / PermissionDenied
Missing valid identity / Permission not allowed
NotFound / AlreadyExists
Related to business resource state
ResourceExhausted
Quota or capacity limits; need throttling or budget adjustment
Unavailable
Might be temporarily unavailable, but still need to judge if the operation can be safely retried
DeadlineExceeded / Cancelled
The caller stopped waiting; does not prove the remote end didn't produce effects
Clients should have a deadline; the total budget must cover queuing, connection, processing, and possible retries. Request::set_timeout writes to the grpc-timeout metadata, which is not the same operation as setting a local transport timeout only; the setting above occurs after connect, so it does not set a two-second limit for the connection establishment itself.
After service cancellation, the application must also stop background work it created. When sending RPCs downstream, propagate the remaining budget instead of giving each layer a full timeout; different languages and libraries have different levels of support for propagation, which must be explicitly verified. gRPC Deadlines
Streaming Lifecycles Are Longer
When the server returns Response<Stream>, it only indicates that a response stream has been established, not that all business messages are complete. Subsequently, stream elements are usually Result<Message, Status>; the consumer must distinguish between normal termination and errors, and cannot silently swallow errors with while let Some(Ok(...)).
Sending to a bounded mpsc channel via send().await can feed back slow consumers to the producer. A send failure indicates the receiver has disappeared; you should end the corresponding work instead of unwrap; unbounded queues might turn network backpressure into memory growth. Background tasks also need to hold and respond to appropriate cancellation signals.
Message size limits are not equal to the total size limit of the entire stream. Long streams also require duration, total bytes, concurrency, or business window controls; if the client only pulls part of the data and discards it, the server should also release associated resources.
Verify Producer and Consumer Termination Protocols In-Process
Add "sync" to the previous tokio dependency features, then save the following src/bin/stream_state.rs. It uses a bounded channel common to the production side of streaming services, directly observing Result in the queue; this step does not require listening on a port or calling a remote service.
usetokio::sync::mpsc;usetonic::{Code, Status};usegrpc_demo::greeter::HelloReply;#[tokio::main]
async fnmain(){let(sender,mut receiver)=mpsc::channel::<Result<HelloReply, Status>>(1);let producer =tokio::spawn(async move{if sender.send(Ok(HelloReply { message:"part-1".into()})).await.is_err(){return;}let_= sender.send(Err(Status::unavailable("source interrupted"))).await;// No more business messages produced after termination error; release sender on exit.
});letmut received =Vec::new();letmut failure =None;whileletSome(item)= receiver.recv().await {match item {Ok(reply)=> received.push(reply.message),Err(status)=>{
failure =Some(status.code());break;}}}drop(receiver);
producer.await.unwrap();assert_eq!(received,["part-1"]);assert_eq!(failure,Some(Code::Unavailable));let(sender, receiver)=mpsc::channel::<Result<HelloReply, Status>>(1);drop(receiver);assert!(sender.send(Ok(HelloReply { message:"unused".into()})).await.is_err());}
A bounded capacity of 1 only constrains the number of queued message items, not the string byte count in one message, nor the objects the producer has already constructed before send. The producer stops sending when it encounters the receiver leaving; if it still holds database cursors or has started other tasks, those resources should be released or cancelled simultaneously.
Actual tonic clients can handle messages, normal termination, and errors separately according to the Result<Option<Message>, Status> of stream.message().await; you cannot treat Err as None. If the business requires a complete snapshot in one go, part-1 in the example above can only be considered temporary data; it should not be submitted as a complete result upon final failure; if the business allows incremental consumption, message sequence numbers, recovery cursors, and idempotent processing are needed to handle reconnections.
Local Verification and Transport Verification Cover Different Issues
You can directly call the generated Greeter trait methods to verify empty name and normal responses, and then use Prost tests for field encoding; these checks quickly locate business and protocol model issues. The transport layer still needs separate checks for metadata, TLS, message limits, deadlines, and stream termination status, because directly calling the handler does not go through tonic's HTTP/2 encoding/decoding and middleware.
Similarly, the metadata generated by the client's set_timeout needs to be correctly understood by the server and relay chains. It does not add budget for DNS/connection waiting before creating the Channel, nor does it mean background tasks will automatically stop. Establish a total deadline around a user operation, and then separately verify connection establishment, RPC waiting, and application resource cleanup.
What Interceptors Are Suitable For
The Interceptor interface handles Request<()> and synchronously returns a request or Status, suitable for reading metadata, injecting lightweight context, and immediate rejection. It cannot directly await remote authentication, nor does it provide arbitrary response body transformation capabilities. Interceptor
When asynchronous external queries, complete observation of requests and responses, or finer-grained flow control are needed, use Tower services suitable for insertion layers, or handle them explicitly in the business service. Regardless of where they are placed, you should clarify how errors become Status and how resources are cleaned up when streams end, see Tower and Middleware.