Files
clean-architecture-frontend…/docs/architecture/decisions/VD-27-grpc-web-unary-and-server-stream.md

45 KiB

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
  • 상위 API 설계: API contract, Schema, Mapper와 Server State

배경

현재 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 예:

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로 고정한다.

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 경계

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

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

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

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로 사용하지 않는다.

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를 권장한다.

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 검증을 생략한다는 뜻이 아니다.

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하지 않는다.
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_filtersenvoy.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_optionsenvoy.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_maxgrpc_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

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-textAccept: application/grpc-web-text를 사용한다. exact selected media type은 transport profile에 고정하고 response media를 검증한다.
  • X-Grpc-Web: 1X-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하지 않는다.

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만으로 결정하지 않는다.

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.codegrpc-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

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가 결정한다.

SAFE
IDEMPOTENT
KEYED_COMMAND
NON_REPLAYABLE
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_ADAPTERSAFE | 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

OPENING
  -> STREAMING
  -> COMPLETING
  -> COMPLETED

OPENING | STREAMING
  -> FAILED | CANCELLED | GAP | OVERFLOW
ServerStreamPort<Event>
  open(bound request, signal)
  -> AsyncIterable<Result<Event>>
  -> 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:

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-status400/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를 별도로 기록한다.

완료 기준

  • 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이 통과한다.