# VD-27: gRPC-Web unary와 server stream - 상태: Accepted design — common coordinator available, wire adapter pending - 결정일: 2026-07-28 - provider-neutral Browser RPC V3 coordinator: `AVAILABLE_NOT_COMPOSED` - gRPC-Web unary/server-stream reference adapter: `DESIGNED_NOT_IMPLEMENTED` - product gRPC-Web composition: `NOT_SELECTED` - client-streaming/bidirectional-streaming guarantee: `PLATFORM_LIMITED` - 관련 결정: VD-13, VD-23, VD-24, VD-25, VD-28, VD-29, VD-30 - 상세 설계: [Protobuf browser transport와 REST Gateway](../protobuf-browser-transport-and-rest-gateway.md) - 상위 API 설계: [API contract, Schema, Mapper와 Server State](../api-contract-schema-mapper-and-server-state.md) ## 배경 현재 source에는 provider-neutral operation/profile registry, application port와 unary/server-stream lifecycle coordinator가 있다. package direct dependency, generated output, `.proto`/descriptor, Buf/protoc config, proxy route와 actual browser test에는 gRPC-Web capability가 없다. browser는 native gRPC transport를 직접 실행하지 않는다. gRPC-Web 또는 선택한 Connect protocol을 지원하는 BFF/Envoy/gateway가 필요하다. protobuf backend가 있다는 이유만으로 browser client에 generated SDK를 노출하지 않는다. gRPC-Web은 unary와 server streaming에 적합할 수 있지만 browser baseline에서 client streaming과 bidirectional streaming을 일반 gRPC처럼 보장하지 않는다. 요구 의미에 따라 upload/job은 REST, one-way server event는 SSE, 진짜 duplex는 WebSocket/WebTransport 또는 별도 protocol로 다시 설계한다. ## 결정 ### 1. Protocol profile을 혼용하지 않는다 선택 가능한 provider profile 예: ```text GRPC_WEB_BINARY GRPC_WEB_TEXT ``` `GRPC_WEB_BINARY | GRPC_WEB_TEXT`는 wire transfer/framing profile이고 Protobuf binary와 JSON message encoding을 뜻하는 이름이 아니다. Connect protocol `CONNECT_V1`은 이 목록에서 선택하지 않으며 VD-29의 Connect state machine과 registry가 별도로 소유한다. gRPC-Web transport implementation도 별도 discriminant로 고정한다. ```text GrpcWebTransportRuntimeKind = OFFICIAL_GRPC_WEB_XHR | CONNECT_WEB_GRPC_WEB_FETCH | CUSTOM_FETCH_FRAMED GrpcWebClientApiKind = CALLBACK_STREAM | PROMISE_UNARY | ASYNC_ITERABLE GrpcWebMessageEncoding = PROTO | JSON ``` 한 operation/provider는 하나의 exact profile에 binding한다. - gRPC-Web framing/status와 Connect envelope/status를 같은 decoder로 추측하지 않음 - content type을 보고 runtime이 vendor를 자동 선택하지 않음 - `OFFICIAL_GRPC_WEB_XHR`는 pinned official `grpc-web` runtime이 XHR, request framing, text base64, response framing과 status 해석을 소유한다. 이를 custom Fetch/raw-frame 경로라고 기록하지 않음 - `CONNECT_WEB_GRPC_WEB_FETCH`는 VD-29가 소유하는 pinned Connect-Web `createGrpcWebTransport()`다. Fetch를 사용하지만 request/response gRPC-Web binary envelope, JSON/Proto serialization, status와 trailer decode를 selected runtime이 소유하므로 `CUSTOM_FETCH_FRAMED` raw decoder로 분류하지 않음 - `CUSTOM_FETCH_FRAMED`만 browser Fetch `ReadableStream`, `AbortController`와 adapter-owned raw frame decoder를 사용함 - official grpc-web runtime의 unary는 binary 또는 text, server-stream은 `grpcwebtext`만 허용 - official runtime의 `SERVER_STREAM + GRPC_WEB_BINARY` registry row는 거절 - Connect-Web gRPC-Web runtime은 binary envelope의 Proto unary/server-stream을 후보로 허용한다. JSON은 exact provider/runtime fixture가 있을 때만 별도 `messageEncoding=JSON` row이고 `GRPC_WEB_TEXT`는 항상 거절한다. - Connect-Web gRPC-Web server stream은 `ASYNC_ITERABLE` API와 Chromium, Firefox, WebKit 및 actual proxy의 incremental-read/cancel/backpressure/trailer evidence가 있을 때만 허용 - custom Fetch binary stream은 selected runtime ID, 세 browser와 actual proxy의 incremental-read/cancel/backpressure evidence가 있을 때만 별도 profile - official production composition은 cancel handle과 terminal status event를 유지하는 `CALLBACK_STREAM`을 기본으로 한다. official `PROMISE_UNARY`는 `.on(...)` metadata/status event와 direct call handle을 제공하지 않는다. pinned runtime/codegen이 `PromiseCallOptions.signal`을 지원하면 cancellation은 그 `AbortSignal`로 충족할 수 있지만, required status/metadata event는 충족하지 못하므로 registry에서 거절 - text mode의 base64 overhead를 message/total budget에 반영 이 ADR의 state machine은 gRPC-Web을 기준으로 한다. Connect-Web runtime이 `CONNECT_V1`을 선택하는 경우는 이 ADR의 runtime row가 아니라 VD-29의 별도 protocol/profile/fixture를 사용하고 gRPC-Web frame을 재사용하지 않는다. ### 2. Application 경계 ```text feature gateway -> registered semantic operation -> GrpcWebOperationDefinition -> generated request mapping -> selected gRPC-Web transport runtime -> runtime-owned OR custom adapter-owned frame/status/trailer validation -> selected runtime/generated response decode -> semantic runtime validation -> boundary mapper -> application result or mapped stream event ``` application/domain/presentation/query cache에 노출하지 않는다. - generated service client - generated protobuf message - service/method string - endpoint/metadata - raw frame/trailer/status details - protobuf bigint/bytes representation ### 3. Operation definition ```text GrpcWebOperationV1 protocol = GRPC_WEB_V1 semanticOperationId providerId clientRuntimeId transportRuntimeKind clientApiKind grpcWebWireSpecRevision transportProfile messageEncoding = PROTO | JSON fullyQualifiedService method rpcKind = UNARY | SERVER_STREAM requestMessageId responseMessageId descriptorArtifactId descriptorDigest requestSemanticSchemaId responseSemanticSchemaId mapperId statusProfileId responseHttpStatusProfileId authProfileId csrfProfileId replayPolicy deadlineProfileId retryProfileId retryOwner = FRONTEND_ADAPTER | ENVOY | NONE proxyProfileId headerBudgetProfileId serverStateProfileId | null maxRequestMessageBytes maxResponseMessageBytes maxResponseMessages maxTotalResponseBytes idleDeadlineMs totalDeadlineMs compressionProfile owner ``` registry가 거절: - client/bidi method - selected `(rpcKind, transportProfile, clientRuntimeId, browser/proxy profile)`이 capability matrix에 없음 - operation/provider의 runtime kind 또는 client API kind가 다름 - official runtime인데 custom Fetch/raw-frame/AbortSignal capability를 요구하거나 custom runtime인데 adapter-owned framing/status decoder가 없음 - official runtime인데 `messageEncoding=JSON`이거나 Connect-Web runtime인데 `transportProfile=GRPC_WEB_TEXT`임 - Connect-Web JSON row에 exact provider media/status/generated-code fixture가 없거나 Connect-Web server stream이 `clientApiKind=ASYNC_ITERABLE`이 아님 - official `PROMISE_UNARY`인데 terminal metadata/status event가 operation requirement이거나 pinned runtime/codegen에 `PromiseCallOptions.signal` capability가 없는데 explicit cancellation이 requirement임 - unknown service/method/message/descriptor - descriptor digest mismatch - unary인데 response max count가 1이 아님 - server stream인데 queue/idle/total/message/count budget 없음 - non-replayable command에 retry - frontend/Envoy/backend 중 retry owner가 둘 이상이거나 selected owner의 exact attempt/backoff/deadline policy가 없음 - executable proxy/header budget profile이 없음 - long-running stream에 ordinary Query profile - caller-provided endpoint/metadata ### 4. Protobuf source와 codegen ```text authenticated proto/Buf module -> immutable source/descriptor set -> provenance + digest -> lint -> breaking comparison against production baseline -> pinned deterministic generation -> adapter-private client/messages -> semantic validators + mapper ``` governance: - field number 재사용 금지 - removed field number/name reserve - package/service/method full name 안정성 - proto3 presence/optional 의미 - enum zero/unknown numeric value - oneof absence/unknown case - map ordering을 application identity로 사용하지 않음 - bytes/message nesting/repeated count ceiling - Timestamp/Duration valid range/nanos - int64/uint64 projection policy - unknown fields library behavior와 mapper projection generated decoder success는 semantic domain proof가 아니다. VD-24 runtime semantic validator가 range, presence, enum/oneof와 collection budget을 확인한다. CI: - pinned Buf/protoc/plugin/runtime/Node versions - source/descriptor/generated digest - clean regenerate diff 0 - Buf lint/breaking - generated import boundary - N/N-1 descriptor fixture - generated code dependency/SBOM/removal ### 5. Provider endpoint와 proxy ```text GrpcWebProviderProfile providerId fixedHttpsOrigin pathPrefix clientRuntimeId transportRuntimeKind clientApiKind grpcWebWireSpecRevision transportProfile messageEncoding browserCapabilityProfileId proxyCapabilityProfileId proxyProfileId headerBudgetProfileId responseHttpStatusProfileId credentialsMode corsProfileId referrerPolicy redirect = ERROR maximumEnvelopeBytes ``` operation과 provider의 runtime kind/client API/runtime version/wire spec/transport/ message encoding/proxy/header tuple은 exact match해야 한다. moving `latest` wire spec이나 library auto-detection을 compatibility proof로 사용하지 않는다. ```text GrpcWebHttpStatusProfile allowedGrpcMediaHttpStatuses[] missingGrpcStatusMap = CANONICAL_HTTP_TO_GRPC_V1 emitSyntheticGrpcStatusWireHeader = false httpGrpcConsistencyFixtureId ``` status/media/source matrix: - valid `grpc-status`가 있으면 HTTP status와 무관하게 그 값이 authoritative다. accepted gRPC-Web media와 terminal source가 있으면 bounded decode한 뒤 selected provider의 HTTP↔gRPC consistency fixture를 별도로 검사한다. - `grpc-status`가 없으면 client는 wire response를 변조하지 않고 아래 canonical HTTP→gRPC mapping으로 **internal normalized status**를 만든다. synthetic response header를 생성하거나 이 표를 server-side gRPC→HTTP 역매핑으로 사용하지 않는다. | HTTP status | missing `grpc-status`의 normalized gRPC status | | --- | --- | | `400` | `INTERNAL` | | `401` | `UNAUTHENTICATED` | | `403` | `PERMISSION_DENIED` | | `404` | `UNIMPLEMENTED` | | `429`, `502`, `503`, `504` | `UNAVAILABLE` | | 그 밖의 모든 status | `UNKNOWN` | - invalid/missing gRPC media, missing terminal source와 malformed body는 위 normalized status와 별개로 `protocolViolation=true`인 transport/contract failure로 기록한다. HTTP 2xx라도 missing `grpc-status`를 success로 만들지 않는다. - `429`를 domain `RATE_LIMITED`로 곧바로 해석하지 않는다. canonical `UNAVAILABLE` normalization 뒤 selected application status mapper가 별도 증거가 있을 때만 rate-limit 의미를 부여한다. - operation/provider `responseHttpStatusProfileId`가 다르면 composition을 거절한다. same-origin BFF/Envoy gateway를 권장한다. ```text GrpcWebCorsProfile topology = SAME_ORIGIN | CROSS_ORIGIN allowedOrigins[] allowCredentials allowMethods = [POST, OPTIONS] allowHeaders[] exposeHeaders[] forwardNotMatchingPreflights = false clientSuppressCorsPreflight = false maxAgeSeconds ``` cross-origin profile과 actual provider evidence: - exact allow-origin, wildcard credential 금지 - `POST`, `OPTIONS` 외 method 금지 - 최소 `content-type`, `x-grpc-web`, `x-user-agent`, selected deadline의 `grpc-timeout`과 exact auth/CSRF/idempotency/trace header만 allowlist - trailers-only response header를 위해 `grpc-status`, `grpc-message`, rich details를 선택한 profile은 `grpc-status-details-bin`까지 expose - official runtime의 preflight 억제/query-parameter metadata 광고를 사용하지 않는다. credential/metadata가 URL, browser history, access log로 이동하지 않게 client option `suppressCorsPreflight=false`로 고정 - Envoy CORS filter는 non-matching preflight를 upstream으로 전달하지 않고 fail-closed한다. - TLS - redirect 없음 - stream buffering 없음 same-origin profile도 CSRF/Fetch Metadata/Origin 검증을 생략한다는 뜻이 아니다. ```text GrpcWebHeaderBudgetProfile maxRequestHeaderListBytes maxResponseHeaderListBytes maxTrailerListBytes maxHeaderFieldCount maxBinaryMetadataDecodedBytes ``` - 각 값은 양의 유한값이며 operation/provider/proxy에서 exact match한다. native gRPC protocol이 제안하는 header-list별 8 KiB를 initial ceiling으로 검토하되 제품이 다른 값을 택하면 actual runtime/proxy fixture와 함께 고정한다. - gateway는 browser/runtime allocation 전에 request/response header hard cap을 집행하고 custom trailer parser는 field count, encoded/decoded bytes cap을 집행한다. official runtime은 raw frame 이전 cap을 application code가 소유한다고 주장하지 않는다. - registered `-bin` metadata만 허용한다. padded/unpadded base64를 처리하고, transport가 duplicate binary header를 comma-join했다면 각 value를 먼저 분리한 뒤 개별 decode한다. unknown metadata는 application에 projection하지 않는다. ```text GrpcWebProxyProfile gatewayKind = ENVOY_GRPC_WEB | SELECTED_BFF gatewayVersion immutableConfigDigest downstreamTlsProfileId upstreamProtocol = HTTP2 filterOrder = [grpc_web, cors, router] routeTimeoutMs = DISABLED | positive-integer hcmStreamIdleTimeoutMs routeIdleTimeoutMs maxStreamDurationMs grpcTimeoutHeaderMaxMs grpcTimeoutHeaderOffsetMs buffering = DISABLED flushProfileId localReplyProfileId retryOwner headerBudgetProfileId ``` Envoy profile은 개념 필드로만 끝내지 않고 pinned v3 config artifact의 다음 위치로 render한다. | 요구 | Envoy v3 config 위치 | | --- | --- | | bridge/filter order | HCM `http_filters`의 `envoy.filters.http.grpc_web`, `envoy.filters.http.cors`, `envoy.filters.http.router` 순서 | | exact operation route | `Route.match`의 registered service/method path와 `RouteAction.cluster` | | upstream HTTP/2 | cluster `typed_extension_protocol_options`의 `envoy.extensions.upstreams.http.v3.HttpProtocolOptions.explicit_http_config.http2_protocol_options` | | total/idle/max duration | `RouteAction.timeout`, `RouteAction.idle_timeout`, `RouteAction.max_stream_duration` 및 HCM `stream_idle_timeout` | | deadline clamp | `RouteAction.max_stream_duration.grpc_timeout_header_max`와 `grpc_timeout_header_offset` | | CORS | route/virtual-host `typed_per_filter_config[envoy.filters.http.cors]`; deprecated `cors` shortcut을 새 profile에 사용하지 않음 | | buffer | `envoy.filters.http.buffer`를 설치하지 않거나 selected route의 `BufferPerRoute`를 disabled | | retry | `RouteAction.retry_policy`; owner가 Envoy가 아니면 absent, hedge policy absent | Envoy grpc_web filter 또는 selected gateway가: - pinned version/config digest와 exact route/service/method allowlist - Envoy일 때 `grpc_web -> cors -> router` filter 순서와 upstream HTTP/2 - exact content type/framing, local reply와 missing-status fixture - message/body/header/deadline cap - terminal trailers/status preservation - server stream flush/backpressure - auth/CSRF/Origin - retry owner와 upstream status mapping 을 staging에서 증명해야 한다. Envoy route timeout 기본값에 기대지 않는다. server stream route는 `timeout: 0s` 또는 application total deadline보다 긴 exact finite value를 선택하고, 별도 finite stream-idle/max-stream-duration cap을 둔다. `grpc_timeout_header_max`와 offset은 proxy가 browser deadline을 늘리지 않고 필요하면 browser보다 먼저 종료하도록 맞춘다. route/HCM idle과 buffer/flush도 명시하며 selected route에서 complete-body buffer filter를 비활성화한다. fake fetch나 15초 미만 fixture만으로 provider conformant가 아니다. ### 6. Request ```text validated application input -> generated request message -> semantic validation -> bounded selected Proto/JSON encode -> transport-owned metadata/auth/CSRF -> final invariant -> selected transport runtime -> OFFICIAL_GRPC_WEB_XHR: runtime-owned frame/base64 + XHR -> CONNECT_WEB_GRPC_WEB_FETCH: runtime-owned frame/status + Fetch -> CUSTOM_FETCH_FRAMED: adapter-owned frame/base64 + Fetch ``` - service/method path는 registry가 만든다. - wire method는 `POST`로 고정한다. - binary Proto baseline은 `Content-Type: application/grpc-web+proto`, Connect-Web JSON provider row는 `Content-Type: application/grpc-web+json`, text baseline은 `Content-Type: application/grpc-web-text`와 `Accept: application/grpc-web-text`를 사용한다. exact selected media type은 transport profile에 고정하고 response media를 검증한다. - `X-Grpc-Web: 1`과 `X-User-Agent`를 selected runtime이 소유한다. browser `User-Agent`를 대신 설정하거나 caller가 이 fixed header를 override하지 못한다. - caller가 endpoint, metadata, `grpc-timeout` 또는 transport header를 제출하지 않는다. - request message byte cap을 selected Proto/JSON encode 전후 검사한다. - official runtime은 request framing/base64를 소유하고 generated client에 validated message만 넘긴다. Connect-Web runtime은 selected Proto/JSON serialization과 binary envelope를 소유하고 VD-29의 bounded `Transport`/Fetch evidence를 충족한다. custom runtime만 encoded Protobuf를 bounded gRPC-Web data frame으로 직접 감싼다. - official grpc-web baseline의 message compression flag는 disabled이며 set bit를 거절한다. unary full-body HTTP `Content-Encoding`은 별도 provider profile과 decoded-body ceiling으로 다룬다. custom message compression은 exact runtime/proxy fixture 없이는 승인하지 않는다. - credential owner가 URL/path/method/body digest를 바꿀 수 없다. - auth-required operation은 unavailable/unauthenticated에서 fetch 0회다. - cookie profile은 POST/custom content type만으로 CSRF가 해결됐다고 보지 않고 exact Origin/Fetch Metadata/CSRF proof를 요구한다. ### 7. Frame decoder raw frame pipeline은 `CUSTOM_FETCH_FRAMED`에만 적용한다. gRPC-Web binary response를 whole `Uint8Array`로 무제한 materialize하지 않는다. ```text Fetch ReadableStream -> bounded incremental frame prefix -> validate flag/type/compression -> validate declared frame length -> bounded exact frame payload -> data message OR trailer block ``` `OFFICIAL_GRPC_WEB_XHR`에서는 pinned official runtime이 XHR body, binary/text framing, base64와 terminal status parsing을 소유한다. adapter가 이미 decoded된 message를 raw frame처럼 다시 parse하지 않는다. wire/header/message hard cap은 gateway/upstream이 allocation 전에 집행하고, adapter는 runtime output의 message-count/semantic/collection/total budget을 다시 검사한다. official runtime이 내부에서 거절해야 하는 malformed framing/status는 pinned-version conformance fixture로 증명한다. `CONNECT_WEB_GRPC_WEB_FETCH`도 selected Connect-Web `Transport`가 Fetch, binary envelope, Proto/JSON decode와 terminal status를 소유한다. stock transport 앞의 bounded Fetch wrapper 또는 verified upstream hook이 response byte ceiling을 집행할 수는 있지만 gRPC-Web frame을 별도 custom decoder로 다시 추측하지 않는다. required pre-decode cap/terminal invariant hook과 malformed corpus를 exact package version에서 증명하지 못하면 VD-29와 이 ADR 모두 `DESIGNED_NOT_IMPLEMENTED`를 유지한다. 검증: - truncated prefix/payload - unknown/reserved flag - negative/overflow/over-limit length - disallowed compression flag - decompressed message cap - trailer before/after allowed data count - duplicate body trailer 또는 body/header terminal source 충돌 - data after terminal trailer - extra bytes after terminal - total message/frame/byte ceiling - idle and total deadline reader는 failure/cancel/limit/terminal 뒤 cancel/release한다. text mode: - `CONNECT_WEB_GRPC_WEB_FETCH`는 이 branch에 들어오지 않고 build/boot 전에 거절한다. - response 전체가 단일 valid base64 entity라고 가정하지 않는다. runtime flush로 padding이 중간에 나타난 뒤 다음 base64 entity가 이어질 수 있으므로 **concatenated padded/unpadded base64 entities**를 수용한다. - custom runtime은 stateful incremental decoder를 사용한다. padding은 해당 entity를 끝내고 decoder quantum을 reset하지만 padding 또는 새 entity가 browser chunk, gRPC-Web frame과 일치한다고 추정하지 않는다. - invalid alphabet/padding/whitespace policy - encoded와 decoded byte cap - browser chunk가 base64 quantum/frame prefix와 일치한다고 가정하지 않음 - decoded byte stream만 공통 bounded frame decoder에 전달하고 `Content-Transfer-Encoding`에 의존하지 않음 - binary와 별도 conformance fixture ### 8. Trailer와 status success는 HTTP success만으로 결정하지 않는다. ```text HTTP admission + valid gRPC-Web frame sequence + exactly one TerminalStatusSource + exactly one valid grpc-status + grpc-status = 0 TerminalStatusSource = BODY_TRAILER_FRAME | ZERO_BODY_TRAILERS_ONLY_RESPONSE_HEADERS ``` custom runtime은 두 source를 raw parser에서 직접 판정한다. official runtime은 pinned runtime이 같은 protocol rule을 집행하고 `CALLBACK_STREAM`의 normalized status event를 adapter가 소비한다. Connect-Web runtime은 VD-29의 bounded `Transport`와 generated Promise/`AsyncIterable` error/terminal contract로 같은 gRPC-Web authority를 보존한다. 두 runtime 모두 raw source를 application code가 직접 관찰했다고 거짓으로 기록하지 않고 malformed/missing/duplicate source fixture를 selected runtime conformance에서 통과시킨다. missing `grpc-status`로 section 5의 HTTP mapping을 실행한 결과는 valid terminal source나 success가 아니다. `BODY_TRAILER_FRAME`과 response-header `grpc-status`가 동시에 있거나 값이 충돌하면 contract failure다. trailers-only header source는 body/data frame이 0개일 때만 허용한다. 이후 unary/server-stream cardinality를 별도로 판정한다. trailer/status validator: - bounded ASCII/header parser - body trailer name은 lowercase만 허용하고 모든 source는 case-normalized duplicate detection - allowed name/value syntax와 `GrpcWebHeaderBudgetProfile`의 bounded field count/encoded/decoded bytes - `grpc-status` exactly once, canonical decimal `0..16` - percent-decoded grpc-message byte cap - malformed `grpc-message`는 raw 노출 없이 safe replacement/omission하되 valid authoritative status 자체를 잃지 않음 - unknown/raw trailer 폐기 - `grpc-status-details-bin`은 count/decoded byte cap과 allowlisted protobuf `Any` type만 허용 - decoded outer `google.rpc.Status.code`가 `grpc-status`와 exact match - rich details는 non-OK status에서만 허용 raw `grpc-message`, status details와 metadata를 application/log에 보내지 않는다. ### 9. Unary state machine 순서가 있는 판정: 1. network/CORS/final URL 실패처럼 HTTP response 자체가 없으면 exact transport failure mapping이며 gRPC status가 있다고 가장하지 않는다. 2. invalid/unaccepted gRPC-Web media, malformed local reply 또는 redirect는 body를 gRPC frame으로 추측하지 않고 contract/transport failure로 닫는다. valid exposed status가 없으면 section 5의 canonical HTTP→gRPC normalization도 함께 기록한다. 3. accepted gRPC-Web media는 profile이 허용한 HTTP status에서 frame과 terminal source를 bounded decode한다. invalid/truncated/oversize frame은 contract failure. 4. accepted media인데 valid terminal source 또는 `grpc-status`가 없으면 canonical HTTP→gRPC normalized status와 `protocolViolation=true`로 실패한다. 모든 2xx와 mapping 표에 없는 status는 `UNKNOWN`이며 cache/data success는 0이다. 5. HTTP status와 terminal source/status matrix mismatch: contract failure. 6. non-OK terminal status: status mapping, data/cache success 0. 7. OK status + data message 0개: contract failure. `google.protobuf.Empty`도 payload length가 0인 data frame **하나**다. 8. OK status + data message 정확히 1개: generated decode → semantic schema → mapper → generation fence → success. 9. OK status + data message 2개 이상: contract failure. trailers-only non-OK response는 safe failure가 될 수 있다. trailers-only OK + unary zero message는 contract failure다. non-OK에서 받은 data message를 partial success로 사용하지 않는다. server-stream의 zero-event + OK는 stream profile이 허용할 수 있다. ### 10. Status mapping operation status profile이 exact allowlist/mapping을 소유한다. | gRPC status | 기본 safe mapping | | --- | --- | | `OK` | success state machine 계속 | | `CANCELLED` | local signal이면 `REQUEST_ABORTED`, 아니면 provider failure | | `DEADLINE_EXCEEDED` | `REQUEST_TIMEOUT` | | `UNAUTHENTICATED` | `AUTH_REQUIRED` | | `PERMISSION_DENIED` | `FORBIDDEN` | | `NOT_FOUND` | `NOT_FOUND` 또는 existence-hiding | | `ALREADY_EXISTS` | `CONFLICT` | | `ABORTED` | `CONFLICT`/optimistic concurrency | | `FAILED_PRECONDITION` | typed precondition/contract profile | | `INVALID_ARGUMENT`, `OUT_OF_RANGE` | `VALIDATION_REJECTED` | | `RESOURCE_EXHAUSTED` | `RATE_LIMITED` 또는 bounded resource failure | | `UNAVAILABLE` | retry-eligible server failure | | `UNIMPLEMENTED` | provider/contract incompatible | | `INTERNAL`, `UNKNOWN` | safe server failure | | `DATA_LOSS` | integrity/contract failure, retry default off | validation details는 allowlisted `google.rpc.BadRequest` 같은 registered message type과 bounded field path/code만 projection한다. arbitrary `Any` type URL/message를 decode하거나 application에 전달하지 않는다. ### 11. Deadline과 cancellation ```text total logical deadline -> local scheduler -> remaining attempt deadline -> OFFICIAL_GRPC_WEB_XHR -> absolute Unix epoch-ms `deadline` metadata -> official runtime-owned grpc-timeout/XHR timer -> returned call.cancel() | pinned PromiseCallOptions.signal -> CONNECT_WEB_GRPC_WEB_FETCH -> positive remaining timeoutMs -> runtime-owned grpc-timeout/Fetch timer -> AbortSignal + AsyncIterator close -> CUSTOM_FETCH_FRAMED -> bounded grpc-timeout wire metadata -> Fetch AbortController -> response reader/idle timer ``` deadline은 auth attach, network, retry/backoff, frame read, protobuf decode, semantic validation과 mapper를 포함한다. - official runtime public API에는 현재 시각 기준 남은 duration이 아니라 absolute Unix timestamp milliseconds인 `deadline` metadata를 전달한다. pinned runtime이 이를 wire `grpc-timeout`과 XHR timer로 변환한다. - Connect-Web runtime에는 VD-29의 positive remaining `timeoutMs`와 local `AbortSignal`을 전달하고 runtime이 wire `Grpc-Timeout`을 소유한다. `timeoutMs` 0 이하를 timeout 없음으로 해석할 수 있으므로 remaining이 0 이하이면 runtime을 호출하지 않는다. - custom runtime만 remaining duration을 `grpc-timeout`으로 encode한다. 값은 최대 8자리 양의 정수와 `H | M | S | m | u | n` unit grammar를 지키고 local remaining deadline을 늘리지 않게 clamp한다. - retry/auth recovery의 각 physical attempt 직전에 elapsed time을 차감해 absolute deadline 또는 `grpc-timeout`을 다시 계산한다. 최초 attempt 값을 재사용하지 않는다. - attempt timer는 remaining total보다 길지 않음 - server stream은 idle + total deadline 둘 다 - Envoy의 route/max-stream/`grpc_timeout_header_max`/offset은 frontend deadline을 늘리지 않는다. proxy가 먼저 끊는 profile이면 그 차이를 명시하고 normalized terminal reason을 검증한다. - official callback unary/server-stream은 보관한 returned call의 `cancel()`, Promise unary는 pinned runtime/codegen이 지원하는 `PromiseCallOptions.signal`로 중단한다. Promise API에 direct call handle이나 `.on(...)` status/metadata event가 있다고 가장하지 않는다. Connect-Web call은 `AbortSignal`과 server-stream `AsyncIterator` close로 중단한다. custom call은 `AbortController.abort()`와 reader cancel/release로 중단한다. - caller/navigation/logout/runtime close를 별도 cancellation reason으로 유지 - local abort가 server command rollback을 보장하지 않음 - gateway는 downstream disconnect를 upstream cancel로 전달하고 backend handler는 cancellation을 cooperative하게 관찰하며 downstream RPC에도 remaining deadline과 cancel을 전파함 - timer/listener/reader를 모든 terminal path에서 정리 ### 12. Replay와 retry 모든 gRPC method가 HTTP POST여도 semantic safety는 method registry가 결정한다. ```text SAFE IDEMPOTENT KEYED_COMMAND NON_REPLAYABLE ``` ```text GrpcWebRetryProfile retryOwner = FRONTEND_ADAPTER | ENVOY | NONE maxPhysicalAttempts maxSleepMs maxElapsedMs initialBackoffMs maxBackoffMs backoffMultiplier jitterProfile retryableTransportOutcomes[] retryableGrpcStatuses[] retryPushbackProfile = UNSUPPORTED | SELECTED_EXTENSION hedging = DISABLED ``` gRPC-Web protocol 자체는 browser retry를 규정하지 않고 official JS runtime은 native gRPC service-config retry를 제공하지 않는다. 따라서: - `FRONTEND_ADAPTER`는 application/provider extension retry다. Envoy/BFF/Query/ downstream service의 같은 logical operation retry를 끄고 physical attempt를 adapter가 계수한다. - `ENVOY`를 선택하면 frontend와 Query retry를 끈다. exact route retry policy, per-try timeout과 total route/deadline budget을 고정한다. Envoy가 `grpc-status` response header에 대해서만 gRPC status retry를 할 수 있고 response trailers의 status로는 retry하지 못한다는 제약을 capability matrix에 기록한다. server stream route retry/hedging은 금지한다. - `NONE`이면 모든 layer의 automatic retry를 끈다. - caller-provided `x-envoy-*` retry/timeout header는 gateway에서 제거한다. - `grpc-retry-pushback-ms` 같은 metadata는 기본 `UNSUPPORTED`다. exact runtime/provider/proxy fixture와 bounded parser를 가진 selected extension일 때만 사용하며 native gRPC transparent retry라고 표시하지 않는다. - route timeout은 모든 Envoy attempt를 포함한다. per-try와 backoff가 logical total deadline을 넘어가지 않으며 frontend/Envoy/backend의 retry multiplication을 fault injection으로 거절한다. unary: - safe/idempotent 또는 backend-proven keyed operation만 retry - selected transient network/`UNAVAILABLE`/approved resource condition만 - deadline/schema/status/frame/mapper failure retry 금지 - total attempt/sleep/elapsed ceiling - hedging은 모든 production profile에서 off auth recovery: - `UNAUTHENTICATED`에서 replay-safe operation만 one-time single-flight refresh - same request semantic input/idempotency/deadline keyed command: - logical command key + principal + service/method + deterministic request digest - backend atomic dedupe/status/TTL evidence - ambiguous outcome은 new key retry가 아니라 reconcile server stream: - Envoy retry는 off다. 첫 application event 전달 전 frontend opening retry는 `retryOwner=FRONTEND_ADAPTER`인 `SAFE | IDEMPOTENT` operation만 original total deadline, monotonic physical-attempt와 sleep cap 안에서 가능 - `KEYED_COMMAND | NON_REPLAYABLE` server stream은 explicit backend resume/reconcile/dedupe profile이 없으면 registry에서 거절 - event 전달 뒤 transport-only automatic replay 금지 - resume하려면 backend sequence/snapshot/resume token protocol 필요 ### 13. Server-stream state machine ```text OPENING -> STREAMING -> COMPLETING -> COMPLETED OPENING | STREAMING -> FAILED | CANCELLED | GAP | OVERFLOW ``` ```text ServerStreamPort open(bound request, signal) -> AsyncIterable> -> close() ``` 각 data frame: 1. message/frame/total budget 2. generated protobuf decode 3. semantic schema 4. boundary mapper 5. scope/runtime generation 6. sequence/snapshot protocol 7. bounded queue admission `ServerStreamPort`는 use case가 명시적으로 연 operation-bound outbound result로 남을 수 있다. 오직 제품이 runtime-wide external notification projection을 선택한 branch에서만 mapper 이후 event를 VD-28의 common scope/generation/ dedupe/gap/resync coordinator 또는 `FeatureEventInput`에 전달한다. protobuf frame/message를 `REALTIME_EVENT_V1` JSON으로 다시 감싸지 않으며 reconnect는 위 opening/resume 규칙이 계속 소유한다. stream policy: - max messages/bytes/duration - idle deadline - consumer backpressure - bounded queue/high-water mark - overflow outcome - duplicate/gap/order - resume token expiry - terminal status source - cancellation/unsubscribe worker/timer keepalive로 infinite correctness를 주장하지 않는다. ### 14. Query cache integration unary: - mapped application result만 VD-25 TanStack query에 admission - service/method/descriptor/protobuf bytes를 query key/value에 넣지 않음 - `retryOwner` 값과 무관하게 Query retry는 false다. `NONE`을 Query가 암묵적으로 대체하지 않으며 selected gRPC-Web retry owner만 physical attempt를 만든다. finite server-stream aggregate: ```text mapped events -> bounded staging reducer -> valid terminal OK status -> completeness/integrity/schema -> generation fence -> immutable aggregate -> atomic Query commit ``` long-running stream: - ordinary queryFn 아님 - registered reducer가 bounded snapshot을 만들거나 invalidation hint 발행 - event history 무한 cache 금지 - partial/truncated/gap stream을 complete query success로 쓰지 않음 - reconnect를 Query retry로 하지 않음 ### 15. Protobuf mapping VD-24 scalar policy를 사용한다. - int64/uint64를 JS number로 자동 변환하지 않음 - safe range를 증명하거나 decimal string/application value로 mapping - Timestamp/Duration range/nanos 확인 - bytes copy/size/classification - map ordering 독립 - enum zero/unknown numeric - oneof presence - wrappers/optional/default distinction - unknown field를 domain에 passthrough하지 않음 - generated message mutation/reuse 금지, immutable application projection 생성 ### 16. Client/bidi streaming primary status는 `PLATFORM_LIMITED`. 다음으로 fallback/re-design한다. | 요구 | 대안 | | --- | --- | | large upload stream | REST/presigned multipart upload | | browser event duplex | WebSocket/WebTransport 또는 별도 duplex protocol | | server→client one-way + 분리 가능한 command | SSE + registered HTTP command | | command sequence | server-owned job/session API | | telemetry batch | bounded unary/REST batch | generated service가 client/bidi RPC를 가진다는 이유로 browser에서 unary loop로 흉내 내지 않는다. ordering/backpressure/half-close semantics가 달라진다. ### 17. Security와 privacy - fixed endpoint/service/method - caller metadata/header 금지 - `x-envoy-*`, transport/deadline header caller override 제거 - preflight를 피하려 metadata/credential을 query parameter로 이동 금지 - credential/CSRF adapter confinement/final invariant - request/response header, trailer, frame, message, decompressed cap - rich status details allowlist - raw grpc-message/payload/metadata/trailer log 금지 - generated debug JSON/stringifier를 production logging에 사용 금지 - server/proxy authorization와 method-level resource authorization - frontend protobuf validation을 authorization으로 간주 금지 - query/cache에 generated message/token/metadata 없음 ### 18. Observability 허용: - semantic operation ID - allowlisted service/method profile ID - gRPC status bucket + HTTP status group - logical/physical attempt, auth recovery, deadline bucket - request/response message/total bytes/count bucket - stream terminal reason/idle/gap/overflow bucket - descriptor/provider/browser version low-cardinality bucket - cache admission/invalidation outcome 금지: - message/proto JSON - metadata/trailer raw value - grpc-message/status-details bytes - resource/account/request ID actual value - resume token/sequence high-cardinality value ### 19. Testing protobuf/codegen: - source provenance/digest - Buf lint/breaking - field reserve/presence/enum/oneof/int64/time fixture - clean generation - generated import boundary/removal frame/status: - official XHR, Connect-Web runtime-owned Fetch와 custom raw-frame Fetch runtime의 framing/status 책임이 섞이지 않음 - Connect-Web Proto/JSON binary-envelope row와 `grpc-web-text` rejection - partial prefix/payload - unknown flag/over-limit length - compressed/decompression overflow - body trailer vs trailers-only header source, missing/duplicate/conflicting status - missing `grpc-status`의 `400/401/403/404/429/502/503/504/other` canonical mapping, valid status 존재 시 HTTP mapping 미적용, server-side 역매핑 금지 - status canonical `0..16`, body lowercase names, rich-details outer-code mismatch - data after trailer/extra bytes - unary 0/1/2 messages including one zero-length `Empty` frame - non-OK with data - text base64 quantum/padding을 browser chunk 여러 개로 분할 - padded entity 뒤 추가 data/trailer entity, 연속 padded/unpadded entity와 invalid alphabet/padding/whitespace - header/trailer field-count/byte overflow, padded/unpadded `-bin`, comma-joined duplicate binary metadata - rich details allowlist/redaction deadline/retry/auth: - official absolute epoch-ms `deadline` metadata, returned call `cancel()`과 pinned Promise `AbortSignal` - Connect-Web positive remaining `timeoutMs`, AbortSignal과 AsyncIterator close - custom 8-digit/unit `grpc-timeout`, Fetch abort와 reader cancel - headers 전/후와 첫 stream event 전/후 caller cancel, attempt/total/idle timeout - retry/auth recovery마다 remaining deadline 재계산, Envoy timeout max/offset ordering - reader/timer/listener cleanup - UNAUTHENTICATED single-flight - SAFE/IDEMPOTENT/KEYED_COMMAND/NON_REPLAYABLE - `FRONTEND_ADAPTER | ENVOY | NONE` exact-one owner와 Query retry off - Envoy response-header `grpc-status` retry와 trailer-status no-retry, route total budget - transient status exact retry, pushback unsupported default, `x-envoy-*` caller strip - frontend/Envoy/backend retry amplification과 hedging 거절 - ambiguous command reconcile server stream: - zero/many events - backpressure/high-water/overflow - duplicate/order/gap/resume - terminal/non-terminal/truncated - account/logout/generation - finite aggregate atomic cache commit provider/browser: - runtime kind/client API/spec/rpcKind/wire-profile capability matrix와 official Promise status-event rejection/`PromiseCallOptions.signal` version capability - Connect-Web unary Promise/server-stream `ASYNC_ITERABLE`, Proto/JSON exact media와 `GRPC_WEB_TEXT` rejection - exact POST/media/Accept/`X-Grpc-Web`/`X-User-Agent` wire header - pinned actual Envoy/BFF version/config digest, upstream HTTP/2와 `grpc_web -> cors -> router` order - exact-origin CORS positive/negative preflight, credential/CSRF, exposed status headers, non-matching preflight fail-closed와 preflight suppression 금지 - local reply/missing-status, compression/message/header/deadline cap - stream이 15초를 넘는 fixture, explicit route/idle/max-stream timeout과 no buffering/flush - Chromium/Firefox/WebKit cancellation/status/trailer/backpressure - Safari/enterprise proxy buffering and selected text fallback MSW/memory frame fixture는 actual gateway evidence가 아니다. ### 20. Rollout 1. product가 protobuf-first operation family와 gateway owner를 선택한다. 2. proto/descriptor/proxy/auth/status/deadline contract를 확정한다. 3. selected runtime별 official-XHR, Connect-Web runtime-owned Fetch 또는 custom raw-frame Fetch transport adapter와 generated boundary를 구현한다. 세 runtime의 framing/cancel 책임을 합치지 않는다. 4. unary read operation을 REST shadow와 비교하되 secondary cache/UI write 금지. 5. actual proxy/browser conformance 통과. 6. reference runtime을 `AVAILABLE_NOT_COMPOSED`로 판정. 7. product composition 뒤 `COMPOSED`, `TrafficAdmission=DISABLED`로 시작. 8. unary safe read internal canary. 9. keyed command는 backend dedupe/reconcile 뒤 별도 canary. 10. finite server stream은 unary와 별도 evidence/traffic gate. 11. client/bidi는 계속 `PLATFORM_LIMITED`. rollback: - 신규 operation/stream admission 중지 - read/stream cancel, command effect reconcile - current scope gRPC-mapped cache clear/invalidate - coherent frontend/generated descriptor/proxy/backend rollback - approved REST read fallback은 새 logical query로만 실행 ### 21. Removal 1. operation traffic과 backend method retirement window 시작 2. unary/stream cancel, command reconcile 3. Query cache/reducer/invalidation listener clear 4. operation/schema/mapper/query profile 제거 5. generated files, proto/descriptor, generator/runtime dependency 제거 6. proxy route/CORS/config 제거 7. backend method는 N/N-1 client window 뒤 제거/reserve 8. production module/dependency/SBOM/removal test 통과 ## 규범 기준 아래 링크는 upstream 기준 위치다. production profile과 evidence는 `master`/`latest` 문자열이 아니라 검증한 immutable commit, package version과 Envoy release를 별도로 기록한다. - [gRPC-Web protocol delta](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md) - [Official grpc-web runtime support matrix](https://github.com/grpc/grpc-web) - [Official grpc-web browser features와 CORS](https://github.com/grpc/grpc-web/blob/master/doc/browser-features.md) - [Official grpc-web XHR/deadline implementation](https://github.com/grpc/grpc-web/blob/master/javascript/net/grpc/web/grpcwebclientbase.js) - [Official grpc-web cancellation handle](https://github.com/grpc/grpc-web/blob/master/javascript/net/grpc/web/clientreadablestream.js) - [VD-29 Connect-Web과 browser Protobuf runtime](./VD-29-connect-web-and-browser-protobuf-runtime.md) - [Connect-Web gRPC-Web transport source](https://github.com/connectrpc/connect-es/blob/main/packages/connect-web/src/grpc-web-transport.ts) - [gRPC HTTP/2 protocol와 timeout/header 규칙](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-HTTP2.md) - [Canonical HTTP→gRPC status mapping](https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md) - [gRPC deadline guide](https://grpc.io/docs/guides/deadlines/) - [gRPC cancellation guide](https://grpc.io/docs/guides/cancellation/) - [gRPC retry guide](https://grpc.io/docs/guides/retry/) - [gRFC A6 client retries](https://github.com/grpc/proposal/blob/master/A6-client-retries.md) - [Envoy gRPC-Web filter](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_web_filter) - [Official grpc-web Envoy example](https://github.com/grpc/grpc-web/blob/master/net/grpc/gateway/examples/echo/envoy.yaml) - [Envoy timeout guidance](https://www.envoyproxy.io/docs/envoy/latest/faq/configuration/timeouts.html) - [Envoy route timeout API](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route_components.proto.html) - [Envoy router retry constraints](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/router_filter) - [Envoy CORS filter](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/cors_filter) - [Envoy CORS API](https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/cors/v3/cors.proto.html) - [Envoy buffer filter](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/buffer_filter) - [Buf breaking-change policy](https://buf.build/docs/breaking/) ## 완료 기준 - exact provider/profile/service/method/descriptor에 operation이 binding된다. - official XHR/Connect-Web runtime-owned Fetch/custom raw-frame Fetch runtime kind, client API, runtime version과 gRPC-Web wire-spec revision이 고정되고 책임이 섞이지 않는다. official runtime의 binary server stream, Connect-Web의 text와 evidence 없는 JSON/server-stream 조합이 거절된다. - missing `grpc-status`가 canonical HTTP→gRPC 표로 failure normalization되고 wire header 합성이나 reverse mapping을 하지 않는다. - pinned executable Envoy/BFF profile이 upstream HTTP/2, filter order, CORS, route/idle/max-stream/deadline, buffering, header budget과 config digest를 닫는다. - deadline/cancel dialect가 runtime별로 고정되고 retry owner가 frontend/Envoy/none 중 정확히 하나이며 Query/backend와 amplification을 만들지 않는다. - text runtime은 concatenated padded/unpadded base64 entity와 arbitrary browser chunk 경계를 처리한다. - protobuf generated type이 adapter 밖으로 나오지 않는다. - frame/message/trailer/status/deadline/cancel/retry가 closed state machine이다. - unary success가 exact one message + body terminal trailer의 유일한 OK status로 증명되고, trailers-only header status는 zero-message unary를 success로 만들지 않는다. - stream이 bounded backpressure/sequence/gap/resume/terminal 계약을 가진다. - int64/time/enum/oneof/bytes semantic mapper가 닫힌다. - actual proxy와 Chromium/Firefox/WebKit evidence가 유효하다. - client/bidi를 지원한다고 표시하지 않는다. - cache에 raw/generated/partial stream state가 없다. - kill switch, coherent rollback과 dependency/proxy/generated removal drill이 통과한다.