Day 118 — Deadlines, Metadata & Status Codes¶
Month 5 · Week 1 · ⬅ Day 117 · Day 119 ➡ · Journal index
🎯 Learning Objective¶
Use the three cross-cutting concerns of every RPC correctly: propagate deadlines via context, attach/read per-call metadata, and return/inspect rich status-code errors.
📚 Topics¶
- Deadlines/timeouts with
context.WithTimeout,grpc-timeout,DeadlineExceeded metadata.MD(outgoing/incoming),status/codes, status details
📖 Reading / Sources¶
📝 Notes¶
- A deadline is absolute, a timeout is relative; the client sets one and gRPC ships it downstream as the
grpc-timeoutheader. Every hop shares the same absolute deadline, so always derive child contexts, never reset them → [[context]]. - When the deadline passes, in-flight work is cancelled and the call ends with
codes.DeadlineExceeded; the server should pollctx.Err()and bail early instead of doing doomed work → [[deadlines]]. - Metadata is
metadata.MD=map[string][]stringwith case-insensitive keys. Client attaches it withmetadata.NewOutgoingContext; server reads it withmetadata.FromIncomingContext. Binary values use a-binkey suffix → [[metadata]]. - Reserved
grpc-*headers (grpc-timeout,grpc-status, ...) are managed by the framework — don't set them by hand. Use metadata for app concerns likeauthorization. - Status = (
codes.Code, message, details). Code0isOK; everything else is an error. Build withstatus.New(code, msg)/status.Error(code, msg); inspect on the client withstatus.FromError(err)thenst.Code()→ [[status-codes]]. - Server-side cancellation vs. client-side:
context.Canceled(client hung up) is distinct fromDeadlineExceeded(ran out of time). Map your domain errors to the rightcodes.Xso callers can retry intelligently (Unavailableis retryable;InvalidArgumentis not). - gRPC codes have a canonical HTTP mapping (used by grpc-gateway): e.g.
NotFound→404,InvalidArgument→400,PermissionDenied→403,Unauthenticated→401,Unavailable→503.
💻 Code Examples¶
// Client: deadline + metadata on the outgoing call.
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
ctx = metadata.NewOutgoingContext(ctx, metadata.Pairs("authorization", "Bearer abc123"))
u, err := client.GetUser(ctx, &userv1.GetUserRequest{Id: 1})
if st, ok := status.FromError(err); ok && st.Code() == codes.DeadlineExceeded {
log.Println("call timed out")
}
// Server: read metadata, honor the deadline, return a typed status error.
func (s *server) GetUser(ctx context.Context, req *userv1.GetUserRequest) (*userv1.User, error) {
md, _ := metadata.FromIncomingContext(ctx)
if len(md.Get("authorization")) == 0 { // keys are case-insensitive
return nil, status.Error(codes.Unauthenticated, "missing token")
}
if ctx.Err() != nil { // client cancelled or deadline blew
return nil, status.FromContextError(ctx.Err()).Err()
}
u, ok := s.find(req.GetId())
if !ok {
return nil, status.Errorf(codes.NotFound, "user %d not found", req.GetId())
}
return u, nil
}
Deadline propagation rebuilt with
context:examples/month-05/deadline(go run ./examples/month-05/deadline) · the status-code model + HTTP mapping:examples/month-05/statuscodes(go run ./examples/month-05/statuscodes) · metadata + auth:examples/month-05/authmeta(go run ./examples/month-05/authmeta)
🏋️ Exercises / Practice¶
| Exercise | Status | Link |
|---|---|---|
Implement the Code/Status model + HTTPStatus mapping |
✅ | exercises/month-05/week-1/statuscode |
🐛 Mistakes Made¶
- Looked up a metadata key with the exact case I sent it (
Authorization) and missed it —metadata.MDkeys are lower-cased; usemd.Getwhich is case-insensitive. - Created a fresh
context.Background()inside a handler for a downstream call, dropping the inherited deadline. Always derive from the incomingctx.
❓ Open Questions¶
- How much deadline "budget" should each hop reserve so the innermost call still has time? (Pattern: subtract a small per-hop margin, or set per-service timeouts that sum under the client's.)
🧠 Active Recall (answer without looking)¶
- Q: A handler creates
context.Background()to call a downstream service. What did it just break?A
It dropped the incoming deadline, cancellation, and metadata. Downstream work no longer stops when the original caller's deadline expires or it cancels. Always derive the child context from the handler's ctx.
2. Q: Why prefer status.Error(codes.NotFound, ...) over errors.New("not found") from a handler?A
The plain error reaches the client as codes.Unknown, losing semantics. A typed status lets the caller branch on st.Code() (retry Unavailable, don't retry InvalidArgument) and maps cleanly to an HTTP status at a gateway.
🪶 Feynman Reflection¶
Every RPC carries three side-channels besides its payload: a deadline (when to give up), metadata (key/value headers like auth), and a status (a numeric code + message describing the outcome). The deadline rides the context, metadata lives in the context too, and the status replaces Go's free-form error with a small fixed vocabulary every language shares.
🕳️ Knowledge Gaps¶
- Rich error details (
status.WithDetails,errdetailsprotos) for structured failures. - Trailing metadata vs. header metadata timing on streams.
✅ Summary¶
I can set and propagate deadlines, attach and read case-insensitive metadata, and return/inspect status-code errors with the correct HTTP mapping.
⏭️ Next Steps / Prep for Tomorrow¶
- Day 119: consolidate the week, re-quiz, and write the review.
| Time spent | Difficulty | Confidence |
|---|---|---|
| 90 min | 🟦🟦⬜⬜⬜ | 🟦🟦🟦⬜⬜ |
Suggested commit: docs(journal): deadlines, metadata and status codes (day 118)