Files
tech-log-frontend/docs/architecture/decisions/VD-27-grpc-web-unary-and-server-stream.md
T

1066 lines
45 KiB
Markdown

# 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<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:
```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이 통과한다.