You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
ErrorKind.PRECONDITION_FAILED maps to HTTP 412, which RFC 9110 reserves for conditional request headers #87
HTTP_STATUS_BY_KIND maps ErrorKind.PRECONDITION_FAILED to 412 (core/errors.py:59). RFC 9110 §15.5.13 defines 412 narrowly: "one or more conditions given in the request header fields evaluated to false when tested on the server", meaning If-Match, If-Unmodified-Since and similar headers. A business precondition such as "the order is not paid yet" is not that, and a client, proxy or SDK that treats 412 as a failed conditional request (stale ETag: refetch and retry) will misread it.
Example from a service with both transports: an invoice requested for an unpaid order (kind = ErrorKind.PRECONDITION_FAILED) answers HTTP 412 and gRPC FAILED_PRECONDITION.
For comparison, google.rpc.Code maps FAILED_PRECONDITION to HTTP 400, and many APIs use 409 or 422 for "valid request, wrong resource state".
Proposal
Map PRECONDITION_FAILED to 400 (the Google mapping, consistent with the gRPC side) or to 409/422, and call it out in the changelog, since clients may branch on the status.
PRECONDITION_FAILED will render as 400. I looked at 409 and 422 as well. What decides it for me is that every other row of HTTP_STATUS_BY_KIND is already the HTTP status google.rpc.Code gives the kind's gRPC status (INVALID_ARGUMENT 400, ALREADY_EXISTS 409, RESOURCE_EXHAUSTED 429, UNAVAILABLE 503, and so on). 412 is the only row that breaks that rule. With 400, a service that exposes both transports answers the same status whether a client calls the HTTP side directly or goes through grpc-gateway or Envoy's transcoder in front of the gRPC side. 409 is already taken by CONFLICT, and 422 is what FastAPI's request validation answers, so either one would end up shared with another kind anyway.
What we lose: the HTTP column stops being injective, because 400 now means INVALID or PRECONDITION_FAILED. Clients tell those apart by code in the problem body, which the docs already recommend for CONFLICT (duplicate key vs. state conflict). The gRPC column stays injective, and nothing in the code reverses the HTTP table.
Since clients can branch on the status, this ships as a breaking change (fix!:, a minor bump while we're on 0.x), and the BREAKING CHANGE note will say how to keep 412 with a custom error_renderer for anyone who needs it. An inbound HTTPException(412) (a real conditional request) keeps answering 412, because its status is carried as an override.
For the second point, the errors page will state that rule for the whole HTTP column and explain the two rows that need a sentence of their own (CONFLICT → ALREADY_EXISTS, and this one). I'm leaving the gRPC table's code comments alone for now because #90 is editing that file.
Fix is up in #94. PRECONDITION_FAILED now answers 400. Your invoice example gets 400 with code: order_not_paid in the problem body, and the gRPC side stays FAILED_PRECONDITION. A test pins the rule for every kind: the HTTP status must be the one google.rpc.Code gives the gRPC status. 412 was the only row that broke it.
It ships as fix!:, so it lands in the next minor. Anything on your side that branches on 412 for this error should branch on code instead. If a client can't change right away, the errors page now has a short renderer that keeps 412 for that kind. Real conditional requests that raise HTTPException(412) still get 412.
The errors page now explains each row: one rule for the whole HTTP column, plus a paragraph each for CONFLICT → ALREADY_EXISTS and this one.
Activity
AlexeyShalaev commentedon Oct 16, 2026
Agreed, 412 is the wrong row. Plan:
PRECONDITION_FAILEDwill render as 400. I looked at 409 and 422 as well. What decides it for me is that every other row ofHTTP_STATUS_BY_KINDis already the HTTP statusgoogle.rpc.Codegives the kind's gRPC status (INVALID_ARGUMENT 400, ALREADY_EXISTS 409, RESOURCE_EXHAUSTED 429, UNAVAILABLE 503, and so on). 412 is the only row that breaks that rule. With 400, a service that exposes both transports answers the same status whether a client calls the HTTP side directly or goes through grpc-gateway or Envoy's transcoder in front of the gRPC side. 409 is already taken byCONFLICT, and 422 is what FastAPI's request validation answers, so either one would end up shared with another kind anyway.What we lose: the HTTP column stops being injective, because 400 now means
INVALIDorPRECONDITION_FAILED. Clients tell those apart bycodein the problem body, which the docs already recommend forCONFLICT(duplicate key vs. state conflict). The gRPC column stays injective, and nothing in the code reverses the HTTP table.Since clients can branch on the status, this ships as a breaking change (
fix!:, a minor bump while we're on 0.x), and the BREAKING CHANGE note will say how to keep 412 with a customerror_rendererfor anyone who needs it. An inboundHTTPException(412)(a real conditional request) keeps answering 412, because its status is carried as an override.For the second point, the errors page will state that rule for the whole HTTP column and explain the two rows that need a sentence of their own (
CONFLICT→ALREADY_EXISTS, and this one). I'm leaving the gRPC table's code comments alone for now because #90 is editing that file.AlexeyShalaev commentedon Oct 16, 2026
Fix is up in #94.
PRECONDITION_FAILEDnow answers 400. Your invoice example gets400withcode: order_not_paidin the problem body, and the gRPC side staysFAILED_PRECONDITION. A test pins the rule for every kind: the HTTP status must be the onegoogle.rpc.Codegives the gRPC status. 412 was the only row that broke it.It ships as
fix!:, so it lands in the next minor. Anything on your side that branches on 412 for this error should branch oncodeinstead. If a client can't change right away, the errors page now has a short renderer that keeps 412 for that kind. Real conditional requests that raiseHTTPException(412)still get 412.The errors page now explains each row: one rule for the whole HTTP column, plus a paragraph each for
CONFLICT→ALREADY_EXISTSand this one.fix!: ErrorKind.PRECONDITION_FAILED renders as HTTP 400, not 412 (#94)