# Protobuf browser transport와 REST Gateway - 상태: production design accepted, product/provider selection pending - 기준일: 2026-07-28 - 범위: gRPC-Web, Connect-Web, Connect protocol, Protobuf contract/codegen, REST/JSON Gateway - 현재 source 상태: provider-neutral Browser RPC V3 계약·application port·공통 unary/server-stream lifecycle runtime은 `AVAILABLE_NOT_COMPOSED`; vendor runtime/dependency/proto/descriptor/proxy는 없음 - 관련 결정: - [VD-23 API transport selection과 REST execution](./decisions/VD-23-api-transport-selection-and-rest-execution.md) - [VD-24 Runtime schema와 boundary mapper](./decisions/VD-24-runtime-schema-and-boundary-mapper.md) - [VD-25 Server state cache lifecycle](./decisions/VD-25-server-state-cache-lifecycle.md) - [VD-27 gRPC-Web unary와 server stream](./decisions/VD-27-grpc-web-unary-and-server-stream.md) - [VD-29 Connect-Web와 browser Protobuf runtime](./decisions/VD-29-connect-web-and-browser-protobuf-runtime.md) - [VD-30 Protobuf contract와 REST Gateway](./decisions/VD-30-protobuf-contract-and-rest-gateway.md) - backend handoff: [Backend API와 Server State contract](./backend-api-and-server-state-contract.md) - 운영 절차: [API contract와 server-state recovery](../operations/api-contract-and-server-state-recovery.md) ## Installed binding snapshot과 stream cleanup bound (R-01, R-04, R-05, R-06) - `installBrowserRpcContractBindings()`가 registry를 **parse → validate → install** 순서로 처리한다. own data descriptor만 읽어 exact key set으로 null-prototype frozen snapshot을 만들고, 그 snapshot을 검증한 뒤 설치한다. getter/accessor, extra key, symbol key, malformed descriptor, revoked proxy는 composition-time `TypeError`이며 getter는 호출조차 되지 않는다. runtime과 transport call은 이후 snapshot만 읽으므로 validation 이후 registry mutation이 replay policy·deadline·byte ceiling·transport selection을 바꿀 수 없다. - server stream 종료는 transport iterator에 lifecycle authority를 위임하지 않는다. commit/admission generation은 즉시 fence하고 listener는 바로 해제하며, `iterator.return()`은 cleanup **요청**으로서 bound 안에서만 기다린다. 끝나지 않은 cleanup은 관찰만 유지되고(unhandled rejection 없음) application generator는 bound 안에 종료된다. cleanup rejection은 이미 선택된 application failure를 덮지 않는다. - WebSocket text frame은 allocation 전에 admission한다. UTF-16 code unit 길이가 이미 cap을 넘으면 encoder를 만들지 않고 거절하고, 나머지는 early exit하는 code-point 누적으로 센다. valid surrogate pair는 4 bytes, lone surrogate는 `TextEncoder`와 동일하게 replacement 3 bytes다. - clock/fence collaborator 예외는 Result 경계를 벗어나지 않는다. clock 실패는 `SERVER_FAILURE/RPC_RUNTIME_DEPENDENCY_FAILED`, capture 실패는 `SCOPE_GENERATION_CHANGED/RPC_SCOPE_GENERATION_UNAVAILABLE`, `isCurrent` 실패는 fail-closed로 canonicalize하며 listener/timer는 단일 exit path에서 정확히 한 번 해제한다. Browser RPC는 여전히 `AVAILABLE_NOT_COMPOSED`다. 선택된 Connect/gRPC-Web transport는 enqueue-time `maxBufferedBytes`, raw/decompressed ceiling, cancel/closed receipt, terminal framing, target browser와 load behavior를 별도로 증명해야 조립할 수 있다 (R-07). ## 1. 먼저 축을 분리한다 네 이름은 같은 종류의 대안이 아니다. | 축 | 선택지 | 소유하는 것 | | --- | --- | --- | | contract/serialization | Protobuf binary, ProtoJSON | message/service schema, field number, presence, codegen | | browser RPC transport | Connect protocol, gRPC-Web | HTTP framing, media, status/error, timeout, stream terminal | | browser client runtime | Connect-Web, official grpc-web 또는 승인 runtime | Fetch/XHR, generated client, interceptors, cancellation | | HTTP exposure | curated REST BFF, generated gRPC-Gateway, Envoy JSON transcoder | resource URL, method, JSON/status/cache/CORS contract | 예를 들어 Connect-Web은 같은 generated service descriptor로 Connect protocol과 gRPC-Web transport를 모두 만들 수 있다. 반대로 REST Gateway는 Protobuf service를 ProtoJSON HTTP API로 노출할 수 있지만, 그것이 자동으로 좋은 browser REST contract가 된다는 뜻은 아니다. 한 semantic operation은 frontend registry에서 정확히 하나의 active wire profile에 binding한다. runtime이 media type을 보고 Connect/gRPC-Web/REST를 추측하거나 장애 시 다른 protocol로 같은 command를 자동 replay하지 않는다. ## 2. 현재 상태 | capability | current status | 근거 | promotion 필요 | | --- | --- | --- | --- | | Browser RPC V3 공통 계약/runtime | `AVAILABLE_NOT_COMPOSED` | exact operation/profile/schema/mapper/encoder/transport join, unary retry·total deadline·abort, bounded server-stream·idle deadline·terminal, generation fence와 unavailable adapter test | selected generated client를 감싸는 protocol transport, raw-byte cap과 actual provider/browser conformance | | Protobuf schema/codegen | `DESIGNED_NOT_IMPLEMENTED` | `.proto`, `buf.yaml`, descriptor, runtime dependency 없음 | authenticated schema source와 owner | | Connect-Web client runtime | `DESIGNED_NOT_IMPLEMENTED` | `@connectrpc/*`, `@bufbuild/protobuf` 없음 | selected service, provider와 bundle budget | | Connect protocol unary | `DESIGNED_NOT_IMPLEMENTED` | endpoint/profile/fixture 없음 | exact JSON 또는 binary profile | | Connect protocol server stream | `DESIGNED_NOT_IMPLEMENTED` | stream runtime/proxy evidence 없음 | bounded stream protocol과 browser evidence | | gRPC-Web unary/server stream | `DESIGNED_NOT_IMPLEMENTED` | VD-27만 존재 | runtime/proxy/descriptor와 conformance | | REST Gateway reference contract/harness | `DESIGNED_NOT_IMPLEMENTED` | native REST reference만 있고 transcoder/fixture 없음 | selected kind의 deterministic/provider conformance | | product REST Gateway composition | `NOT_SELECTED` | route/provider/owner 없음 | curated BFF 또는 selected transcoder의 제품 승인 | | browser client/bidi stream | `PLATFORM_LIMITED` | request streaming을 target browser 공통 계약으로 보장 못함 | 다른 transport/application protocol | | product traffic | `NOT_SELECTED` | operation/provider/owner 없음 | product ADR와 traffic admission | 이 문서가 추가돼도 dependency나 generated source를 기본 bundle에 넣지 않는다. 공통 runtime source가 생긴 뒤에도 같은 원칙을 유지한다. 공통 runtime은 Connect/gRPC-Web wire를 직접 decode하지 않고 selected transport가 반환한 bounded message/terminal/failure만 처리하므로, 실제 Connect-Web 또는 gRPC-Web adapter가 구현됐다는 증거가 아니다. ## 3. 기본 선택 ### 3.1 권장 순서 1. 기존 REST가 제품 의미와 운영 요구를 충족하면 REST를 유지한다. 2. backend가 Protobuf-first이고 browser RPC가 필요하면 Connect-Web + Connect protocol을 우선 평가한다. 3. backend가 gRPC-Web만 노출하거나 기존 Envoy/gRPC-Web conformance 자산을 재사용해야 하면 Connect-Web의 gRPC-Web transport 또는 official grpc-web runtime 중 하나를 고정한다. 4. public HTTP API, CDN/conditional cache, 링크 가능한 resource URL, broad HTTP tooling이 핵심이면 curated REST BFF를 우선한다. 5. `google.api.http` annotation만으로 제품 REST 의미를 온전히 표현할 수 있을 때만 generated gRPC-Gateway/Envoy transcoding을 선택한다. ### 3.2 선택 matrix | 요구 | 기본 후보 | 주의 | | --- | --- | --- | | 내부 web UI + Protobuf-first backend + unary | Connect-Web/Connect | JSON/binary를 operation profile로 고정 | | 내부 web UI + 기존 gRPC-Web gateway | selected gRPC-Web runtime | runtime별 streaming capability가 다름 | | bounded server→browser stream | Connect server stream 또는 gRPC-Web server stream | idle/total/queue/sequence/resume 필수 | | public/resource-oriented HTTP API | curated REST BFF | Protobuf service shape를 그대로 노출하지 않음 | | 단순 proto HTTP annotation과 broad JSON client | generated REST Gateway | ProtoJSON/status/error semantic fixture 필요 | | HTTP cache/ETag/Range/file download | REST/BFF | RPC transcoder로 억지로 만들지 않음 | | browser client/bidi | WebSocket/WebTransport/별도 session API | Connect protocol 자체 지원과 browser 지원을 혼동 금지 | | 기존 REST backend뿐임 | REST 유지 | Protobuf/gateway를 미래 대비로 추가하지 않음 | Connect가 항상 gRPC-Web보다 우월하거나 REST Gateway가 수동 REST보다 항상 저렴하다고 가정하지 않는다. actual provider, proxy, browser, bundle, observability와 조직 운영 비용을 evidence로 비교한다. ## 4. 공통 application 경계 ```text presentation -> feature application input -> semantic gateway port -> installed operation registry -> REST adapter -> Connect adapter -> gRPC-Web adapter -> transport decode -> semantic runtime schema -> boundary mapper -> immutable application projection ``` 금지: ```text page/use-case -> generated service client page/use-case -> protobuf message or descriptor page/use-case -> transport/base URL/metadata generated message -> TanStack cache raw Connect/gRPC error -> presentation REST gateway DTO -> domain without runtime schema/mapper ``` application port는 `listResources`, `createResource`, `watchJob` 같은 의미를 표현한다. `callRpc(service, method, bytes)`나 `executeProto()`를 노출하지 않는다. ## 5. Protocol-neutral operation과 exact binding ```text ApiOperationContractV3 semanticOperationId owner semantics = QUERY | COMMAND | SERVER_STREAM protocol = REST | CONNECT_HTTP | GRPC_WEB replayPolicy idempotencyKeyPolicy authProfileId csrfProfileId deadlineProfileId retryProfileId requestSemanticSchemaId responseSemanticSchemaId mapperId serverStateProfileId | null dataClassification compatibility protocolBinding ``` 현재 source에서는 설치된 REST V2 registry를 위험한 union migration으로 바꾸지 않고 `BrowserRpcOperationV3` sibling registry를 추가했다. 제품 operation이 선택되면 composition root가 semantic feature gateway 뒤에서 REST 또는 Browser RPC 중 정확히 하나를 bind한다. use-case가 두 registry를 보거나 runtime fallback을 결정하지 않는다. Connect/gRPC-Web binding: ```text BrowserRpcOperationV3 + BrowserRpcProviderProfile providerId clientRuntimeId clientRuntimeVersion transportRuntimeKind = CONNECT_WEB_FETCH | OFFICIAL_GRPC_WEB_XHR | CUSTOM_FETCH_FRAMED clientApiKind = PROMISE | ASYNC_ITERABLE | CALLBACK_STREAM protocolRevision wireProfileId encoding = PROTO_BINARY | PROTO_JSON fullyQualifiedService method rpcKind = UNARY | SERVER_STREAM requestMessageId responseMessageId descriptorArtifactId descriptorDigest errorProfileId corsProfileId compressionProfileId deadlineDialect retryOwner = FRONTEND_ADAPTER | EDGE_PROXY | NONE maxRequestBytes maxHeaderBytes maxHeaderFields maxMessageBytes maxResponseMessages maxTotalResponseBytes idleDeadlineMs | null ``` 구현 경로는 `src/contracts/browser-rpc.ts`, `src/application/ports/browser-rpc`, `src/adapters/browser-rpc`다. 공통 profile은 runtime identity/digest, fixed base URL, descriptor digest, allowed procedure, auth/CSRF/CORS/error/deadline/retry owner와 raw-byte ceiling owner까지 고정한다. `maxHeaderBytes`, media/trailer parsing과 실제 raw queue ceiling은 공통 coordinator가 추측하지 않고 selected wire transport가 VD-27/VD-29에 따라 추가로 집행한다. REST Gateway binding: ```text RestGatewayBindingV1 providerId gatewayKind = CURATED_BFF | GRPC_GATEWAY | ENVOY_TRANSCODER gatewayRuntimeId gatewayRuntimeVersion httpRuleArtifactId httpRuleDigest method relativePathTemplate requestProjection protoJsonProfileId | null responseEnvelopeProfileId statusErrorProfileId cacheConditionalProfileId ``` registry는 다음을 boot/build 전에 거절한다. - protocol/profile/runtime/provider tuple 불일치 - descriptor 또는 HTTP rule digest 불일치 - caller-provided endpoint, method, metadata 또는 message type - `SERVER_STREAM`인데 ordinary Query profile 사용 - browser client/bidi method - Connect와 gRPC-Web decoder의 runtime auto-negotiation - runtime kind와 deadline/cancel/status API가 맞지 않는 client API 조합 - frontend와 edge proxy가 동시에 retry owner인 profile - gateway kind가 다른 두 route의 last-write-wins collision - JSON exposure인데 binary-only compatibility gate만 통과 - non-replayable command retry/fallback - hard ceiling보다 큰 message/frame/deadline/URL ## 6. Protobuf contract source ### 6.1 Source of truth ```text authenticated Proto/Buf module -> immutable source commit/module digest -> lint -> breaking comparison -> FileDescriptorSet -> deterministic codegen -> generated artifact digest -> operation/schema/mapper binding -> release contract set ``` runtime endpoint에서 latest schema를 받아 build하지 않는다. schema update는 review 가능한 explicit workflow이며 source, dependency lock, descriptor, plugin/runtime와 generated output digest를 함께 보존한다. ### 6.2 Evolution - field number 재사용/renumber 금지 - 삭제 field number와 JSON name reserve - package/service/method full name 안정성 - enum zero value와 unknown numeric policy - proto3 optional/edition presence를 명시 - oneof absence/unknown case 처리 - map ordering을 identity/digest로 사용하지 않음 - unknown field가 binary↔JSON conversion에서 보존되지 않을 수 있음을 반영 - Timestamp/Duration range와 nanos - int64/uint64의 JS/ProtoJSON projection - bytes와 repeated/nesting ceiling JSON을 한 곳이라도 사용하면 binary wire compatibility만으로 충분하지 않다. 최소 `WIRE_JSON` breaking category를 요구한다. generated SDK의 import/source compatibility를 외부 소비자가 의존하면 `PACKAGE` 또는 `FILE`을 선택한다. Buf gate와 별도로 domain meaning, authorization, default/presence, pagination, revision과 idempotency semantic diff를 수행한다. ### 6.3 ProtoJSON profile operation은 다음을 고정한다. ```text ProtoJsonProfileV1 emitDefaultValues useProtoFieldNames enumEncoding = NAME | NUMBER ignoreUnknownFields int64Projection bytesProjection wellKnownTypePolicy ``` runtime/library default에 맡기지 않는다. 기본 방향: - request unknown field reject - response는 generated decoder 뒤 known semantic projection만 mapper에 전달 - int64/uint64는 decimal string 또는 bounded application type - bytes는 base64 decode 전후 byte ceiling - non-finite float는 domain 승인 없으면 거절 - Timestamp/Duration은 generated decode 성공 뒤 semantic range 재검증 - `Any`는 allowlisted type URL만 ProtoJSON은 ordinary JSON schema의 임의 union을 대체하지 않으며 binary보다 evolution 보장이 약하다. ## 7. Deterministic codegen과 공급망 선택 branch는 최소 다음을 고정한다. ```text buf.yaml buf.lock buf.gen.yaml schema/module digest protoc or Buf version plugin name/version/revision/digest @bufbuild/protobuf version @connectrpc/connect version @connectrpc/connect-web version Buf image digest FileDescriptorSet digest canonical HttpRule manifest digest generated OpenAPI digest when selected generated output digest ``` CI: - format/lint/breaking - dependency lock과 source provenance - clean checkout generate diff 0 - generated directory 수동 수정 금지 - generated import boundary - descriptor↔generated symbol exact join - generated runtime↔runtime compatibility matrix - canonical HttpRule/OpenAPI semantic diff; custom option을 Buf breaking에 위임하지 않음 - N/N-1 fixture - license/SBOM/vulnerability/secret scan - production bundle inventory와 budget - capability removal 뒤 generated/runtime/proxy reference 0 generated files는 `src/generated` 같은 전역 public API가 아니라 selected feature adapter-private root에 둔다. 여러 feature가 공유하는 service라도 좁은 platform adapter facade만 재사용한다. ## 8. Connect-Web Connect-Web은 browser client runtime이다. `createConnectTransport()`는 Connect protocol, `createGrpcWebTransport()`는 gRPC-Web protocol을 사용한다. 같은 package를 쓴다고 두 wire protocol이 호환되거나 자동 failover 가능한 것은 아니다. Connect-ES v2 generation은 `protoc-gen-es`가 message와 service descriptor를 함께 생성하며 과거 Connect 전용 generator를 신규 toolchain에 넣지 않는다. ### 8.1 Connect unary ```text validated semantic input -> generated request -> bounded encode (ProtoJSON or binary) -> POST fixed /package.Service/Method -> bounded response/error decode -> generated message -> semantic validation -> mapper -> generation fence ``` Connect unary는 bare Protobuf/JSON body와 의미 있는 HTTP status를 사용한다. success content type과 selected encoding을 exact 검증한다. error는 non-2xx JSON Connect error profile로만 decode하며 intermediary HTML/JSON을 Connect error로 추측하지 않는다. stock runtime이 unary body를 whole JSON/ArrayBuffer로 읽는 selected version이면 interceptor가 raw-byte ceiling을 대신한다고 기록하지 않는다. edge의 encoded/ decompressed cap과 platform-owned bounded Fetch transport 또는 해당 exact runtime의 overflow conformance가 있어야 production evidence가 닫힌다. GET은 다음을 모두 만족할 때만 별도 profile로 허용한다. - protobuf method `idempotency_level = NO_SIDE_EFFECTS` - public/non-sensitive bounded request - URL byte ceiling과 canonical encoding - header/credential/preflight 정책 - exact CDN/browser cache key, `Vary`, ETag와 retention - request URL이 log/history/referrer에 노출돼도 허용되는 classification 인증/private query의 기본은 POST다. ### 8.2 Connect server stream Connect streaming은 framed `application/connect+proto|json`이고 HTTP status `200` 안의 final EndStream envelope가 RPC error/trailer authority다. decoder는: - incremental 5-byte envelope prefix - flag/compression/declared length - per-message/decompressed/total/count ceiling - exactly one final EndStream envelope - data after EndStream, missing EndStream와 truncation - bounded error/detail/trailer projection - idle + total deadline - reader cancel/release와 bounded consumer queue 를 검증한다. network EOF는 success가 아니다. stock Connect-Web runtime을 사용하면 exact package version이 이 state machine을 얼마나 집행하는지 conformance로 증명하고, 부족한 hard ceiling은 proxy 또는 custom Transport가 소유한다. selected stock runtime의 streaming output compression은 `IDENTITY_ONLY`로 고정하고 지원하지 않는 compressed envelope를 광고하지 않는다. ### 8.3 Interceptor policy interceptor가 허용되는 책임: - registry-owned auth/CSRF patch - remaining deadline/timeout - fixed low-cardinality diagnostics - exact safe retry coordinator - safe error normalization 금지: - arbitrary URL/service/method rewrite - raw message/error logging - caller metadata passthrough - operation semantic retry를 transport가 임의 결정 - cache write와 domain mapping transport/client는 runtime/scope 단위로 재사용하되 base URL/profile이 다른 provider 사이에서 공유하지 않는다. interceptor array의 선언 순서가 아니라 실제 onion execution order를 manifest와 test에 보존한다. remaining total deadline이 0 이하면 `timeoutMs=0`을 넘기지 않고 호출 전에 local deadline failure로 닫는다. ## 9. gRPC-Web 상세 frame/status state machine은 VD-27을 따른다. - browser는 native gRPC/HTTP2 transport를 직접 사용한다고 가정하지 않는다. - official `grpc-web` runtime은 XHR와 runtime-owned frame/status decoder, `ClientReadableStream.cancel()`을 사용한다. raw `ReadableStream` frame parser와 `AbortSignal`을 이 경로의 frontend 보장으로 기록하지 않는다. - custom Fetch runtime만 `fetch`/`AbortController`/incremental raw-frame decoder profile을 사용한다. 두 runtime은 `transportRuntimeKind`로 분리한다. - gRPC-Web response trailer는 body trailer frame 또는 trailers-only response header에서 해석한다. - `grpc-status`가 있으면 그것이 authoritative다. 없으면 native gRPC의 공식 HTTP→gRPC fallback mapping으로 internal status를 만들며 HTTP success만으로 RPC success를 판정하지 않는다. - official `grpc-web` JavaScript runtime은 binary unary와 text unary/server streaming capability를 구분한다. - Connect-Web gRPC-Web transport를 선택하면 별도 `clientRuntimeId`와 그 runtime의 conformance matrix를 사용한다. - text/base64와 binary를 같은 byte budget으로 취급하지 않는다. - text decoder는 browser chunk 하나를 base64 entity 하나로 가정하지 않고, 중간 padding이 있는 연속 base64 entity를 처리해야 한다. - Envoy `grpc_web` filter 또는 selected gateway의 exact version/config를 provider profile에 binding한다. - Envoy profile은 filter order, upstream HTTP/2, route/idle/max-stream timeout, gRPC timeout offset, buffering/flush, header/message ceiling과 local reply mapping을 고정한다. server stream에는 default route timeout을 그대로 쓰지 않는다. - retry owner는 정확히 하나다. server stream replay/hedge는 금지하고, frontend와 Envoy retry를 동시에 켜지 않는다. gRPC-Web과 Connect stream은 terminal envelope가 서로 다르다. 공통 `ReadableStream` helper를 재사용할 수 있어도 decoder/state machine을 합치지 않는다. ## 10. REST Gateway ### 10.1 세 종류를 분리한다 `CURATED_BFF` - browser/resource contract를 별도로 설계 - REST envelope, status, ETag/304/412, idempotency, pagination과 CORS를 직접 소유 - 내부에서 gRPC/Connect service를 호출할 수 있으나 public DTO는 별도 mapper `GRPC_GATEWAY` - `google.api.http` annotation에서 reverse proxy와 optional OpenAPI를 생성 - proto request field를 path/query/body로 projection - ProtoJSON과 gRPC status mapping을 exact profile로 고정 - initial reference 후보는 unary만이며 REST streaming은 별도 ADR 없이는 선택하지 않음 `ENVOY_TRANSCODER` - descriptor와 `google.api.http` annotation으로 proxy filter가 JSON↔gRPC 변환 - filter/runtime config와 descriptor를 coherent artifact로 배포 - application-specific envelope/cache/idempotency 의미는 별도 filter/BFF 없이는 자동 생성되지 않음 한 route는 하나의 gateway kind만 소유한다. 현재 reference REST의 `{ success, data|error, meta }` envelope와 `200|201`, 향후 `204/304/412` 의미는 direct generated gateway의 기본 출력과 같지 않다. 따라서 기존 reference operation은 curated BFF를 유지한다. direct gateway는 ProtoJSON/HTTP rule/status/error 자체를 새 versioned public contract로 승인한 operation에만 적용한다. ### 10.2 Automatic transcoding의 한계 HTTP annotation은 path/method/body projection을 정의하지만 다음을 자동으로 완성하지 않는다. - frontend의 strict success/failure envelope - product authorization와 existence hiding - idempotency store/effect certainty - CursorPage snapshot 의미 - ETag/If-None-Match/If-Match와 revision CAS - CDN/private cache policy - domain error vocabulary와 validation detail redaction - file Range/streaming download/upload - browser-compatible server event/reconnect protocol - OpenAPI와 runtime response/error rewrite의 자동 coherence 따라서 현재 reference REST envelope에 generated gateway를 바로 연결할 수 없다. gateway adapter가 exact envelope/status를 제공하거나 frontend에 새 operation contract를 versioned로 추가해야 한다. ### 10.3 REST mapping - resource name/path는 stable API meaning을 가져야 한다. - path field를 body/query에도 중복 projection하지 않는다. - unbound fields의 query mapping과 repeated/nested encoding을 fixture로 고정한다. - `body: "*"`는 query surface와 HTTP semantics를 숨길 수 있어 default 금지다. - additional binding collision과 ambiguous path template를 build에서 거절한다. - `.proto` annotation을 기본 source of truth로 삼고 external service config를 병용하면 override precedence와 두 artifact digest를 고정한다. - `generate_unbound_methods`는 기본 금지하며 GET/DELETE body, path field type, path unescape mode와 PATCH FieldMask behavior를 fixture로 고정한다. - Buf generic breaking check가 custom HTTP option의 제품 의미까지 증명한다고 간주하지 않는다. canonical route manifest와 generated OpenAPI의 method/path/ body/query/additional-binding diff를 별도 gate로 검사한다. - `response_body` projection은 전체 response schema와 mapper binding을 별도로 갖는다. - ProtoJSON JSON name/default/enum/int64/unknown policy를 고정한다. - request/response body와 URL ceiling은 transcoder 앞/뒤 모두 적용한다. server-stream을 JSON array로 buffer해 반환하는 transcoder 동작을 realtime stream으로 간주하지 않는다. SSE/NDJSON/streaming JSON이 필요하면 별도 protocol ADR과 framing/content type/terminal contract를 만든다. ## 11. Auth, CSRF와 CORS same-origin BFF를 권장한다. cross-origin이면 protocol별 exact profile이 필요하다. Connect: - POST와 optional GET - `Content-Type`, `Connect-Protocol-Version`, `Connect-Timeout-Ms`, selected compression/auth headers - custom unary trailer를 expose할 때 `Trailer-` gRPC-Web: - POST - `Content-Type`, `Grpc-Timeout`, `X-Grpc-Web`, `X-User-Agent`, selected auth headers - `Grpc-Status`, `Grpc-Message`, `Grpc-Status-Details-Bin` expose REST Gateway: - operation별 method/header/media - bearer 또는 cookie+CSRF profile - conditional/idempotency header allow/expose 공통: - exact allow-origin, credential mode와 `Vary` - wildcard credential 금지 - redirect login/HTML response 금지 - Origin/Fetch Metadata/CSRF를 cookie unsafe method에서 검증 - generated client/caller가 arbitrary metadata를 제출하지 못함 - auth attach 뒤 endpoint/path/method/body digest 불변 ## 12. Deadline, cancellation, retry와 command ```text total logical deadline = auth attach + transport attempts/backoff + body/frame read/decompression + generated decode + semantic validation + mapper ``` - Connect-Web call은 AbortSignal과 bounded timeout을 받는다. - local deadline과 wire timeout 중 더 짧은 값만 사용한다. - stream은 idle/total deadline 둘 다 갖는다. - abort가 backend command rollback을 의미하지 않는다. - generated/runtime interceptor retry와 Query retry를 중복하지 않는다. - `SAFE | IDEMPOTENT | KEYED_COMMAND`의 exact evidence가 있는 operation만 retry한다. - keyed command는 protocol과 무관하게 backend atomic idempotency/reconcile이 필요하다. - gateway의 header/message mapping은 idempotency key를 전달할 뿐 durable dedupe, receipt, retention과 reconcile을 구현하지 않는다. - browser abort는 이미 commit된 command의 rollback 증거가 아니다. edge→upstream cancel/deadline 전파와 backend cooperative cancellation을 staging에서 검증한다. - protocol 장애를 이유로 Connect↔gRPC-Web↔REST command를 자동 replay하지 않는다. - read fallback도 새 logical operation으로 시작하고 old result를 cache에 쓰지 않는다. ## 13. Status와 error mapping common safe vocabulary에 mapping하되 wire authority를 섞지 않는다. | source | authoritative outcome | | --- | --- | | Connect unary | HTTP status + bounded Connect JSON error | | Connect stream | HTTP admission + final EndStream envelope | | gRPC-Web | HTTP admission + terminal grpc-status source | | REST Gateway | selected HTTP/envelope/problem profile | mapping은 `UNAUTHENTICATED/AUTH_REQUIRED`, `PERMISSION_DENIED/FORBIDDEN`, `NOT_FOUND`, `CONFLICT/ABORTED`, validation, rate limit, unavailable, deadline/cancel을 공통 `AppFailure`로 투영한다. raw message, arbitrary Any/detail, metadata/trailer와 vendor error는 application/log에 전달하지 않는다. 같은 semantic backend error라도 transport별 status body가 다를 수 있으므로 conformance suite가 최종 `AppFailure`와 retry/effect certainty가 같은지 검증한다. ## 14. Server State와 cache - query key는 semantic application input에서만 파생한다. - service/method/protobuf bytes/REST URL을 key에 넣지 않는다. - generated message/Connect response를 cache하지 않고 mapper output만 admission한다. - transport retry가 있으면 Query retry는 off다. - scope/generation mismatch result는 폐기한다. - Connect GET/CDN cache, browser HTTP cache와 TanStack cache owner를 operation별로 하나씩 명시한다. - stream event는 ordinary query result가 아니다. - finite stream aggregate는 terminal success 뒤 atomic commit한다. - long-running stream은 bounded reducer 또는 invalidate-only hint를 사용한다. ## 15. Security와 privacy - descriptor/generated code는 신뢰된 source에서만 - fixed provider/service/method/route - message/body/frame/decompressed/collection/depth ceiling - recursive schema와 Any type allowlist - frontend generated validation을 backend authorization으로 간주 금지 - raw payload, ProtoJSON, debug stringifier, metadata/trailer/cursor/revision log 금지 - request/response message에 credential을 넣지 않음 - query cache/persistence에 generated message 없음 - gateway가 unknown field/duplicate JSON key를 어떻게 처리하는지 fixture로 고정 ## 16. Observability 허용: ```text semantic operation ID protocol/client runtime/provider profile ID descriptor/http-rule artifact version HTTP status group / safe RPC code attempt/duration/message-size/count bucket stream terminal/idle/gap/overflow bucket cache admission outcome ``` 금지: - service request/response content - protobuf debug JSON/string - metadata/trailer/error message/detail - URL query/GET message - credential/idempotency/cursor/revision - resource/account identifier actual value protocol별 metric을 비교할 때 semantic operation ID를 join key로 쓰고 raw service/method를 high-cardinality label로 사용하지 않는다. ## 17. Test와 provider evidence ### 17.1 Deterministic - proto format/lint/breaking/descriptor/codegen digest - enum/oneof/presence/int64/time/bytes/unknown-field fixture - Connect unary JSON/binary success/error/media/status - Connect stream partial prefix, length, compression, EndStream, truncation - gRPC-Web binary/text frame/trailer/status matrix - REST annotation path/query/body/response mapping - ProtoJSON default/name/enum/int64/null/unknown behavior - retry/deadline/cancel/auth/idempotency - scope/generation/cache admission - generated import/bundle/removal ### 17.2 Actual provider/browser - selected Connect/gRPC-Web server or Envoy/gateway version - exact CORS/preflight/auth/CSRF - HTTP/1.1/2 proxy buffering and stream flush - media, compression, terminal status/trailer preservation - message/body/time limit - Chromium/Firefox/WebKit cancel/stream/backpressure - REST Gateway N/N-1 and OpenAPI/descriptor coherence - browser abort→gateway context→upstream cancel/deadline propagation - HttpRule route manifest/OpenAPI/runtime response rewrite coherence - kill switch, rollback and dependency/proxy removal drill memory fake/MSW만으로 provider conformance를 주장하지 않는다. ## 18. Rollout ```text product operation selected -> schema/provider/gateway owner -> immutable proto + descriptor -> codegen and semantic mapper -> provider-neutral adapter/fake -> actual gateway/browser conformance -> AVAILABLE_NOT_COMPOSED -> bootstrap TrafficAdmission=DISABLED -> COMPOSED -> read-only shadow -> canary -> enabled ``` - 처음에는 safe unary read 하나만 선택한다. - shadow result는 UI/cache에 쓰지 않는다. - Connect와 gRPC-Web을 동시에 canary하지 않는다. - server stream은 unary와 별도 gate다. - command는 backend idempotency/reconcile 뒤 별도 gate다. - REST fallback은 사전 등록된 read operation만 새 logical query로 실행한다. - rollback은 frontend/generated descriptor/gateway/backend를 coherent set으로 한다. ## 19. Removal 1. 신규 call/stream admission을 닫는다. 2. query/stream cancel, command effect reconcile. 3. cache/reducer/invalidation listener를 clear한다. 4. operation/schema/mapper/provider profile을 제거한다. 5. generated source, descriptor/proto input과 codegen config를 제거한다. 6. Connect/gRPC runtime dependency와 proxy/transcoder route를 제거한다. 7. backend method/HTTP binding은 N/N-1 client window 뒤 retirement한다. 8. bundle, SBOM, lockfile, proxy config와 source reference 0을 증명한다. ## 20. 구현 work package | 순서 | package | exit | | --- | --- | --- | | PB-01 | schema governance | authenticated module, Buf lint/breaking, descriptor/digest | | PB-02 | generated boundary | deterministic TS generation, private imports, mapper | | PB-03 | Connect unary | exact JSON/binary profile, error/deadline/cancel | | PB-04 | gRPC-Web unary | selected runtime/proxy/frame/status conformance | | PB-05 | bounded server stream | queue/idle/total/terminal/sequence/resume | | PB-06 | REST Gateway | selected kind, HTTP annotation/ProtoJSON/error/cache fixture | | PB-07 | operations | browser/provider evidence, canary/kill/rollback/removal | PB-03~06은 제품에서 선택한 branch만 구현한다. “미래 대비”로 모두 설치하지 않는다. ## 21. 완료 기준 - [ ] Protobuf, client runtime, wire protocol과 gateway 축이 분리돼 있다. - [ ] 한 operation은 한 active transport/provider profile만 가진다. - [ ] descriptor/codegen/runtime/gateway artifact가 release digest에 binding된다. - [ ] generated type이 adapter 밖으로 나오지 않는다. - [ ] ProtoJSON과 binary compatibility policy가 각각 닫혀 있다. - [ ] Connect unary/stream과 gRPC-Web decoder가 서로의 terminal 규칙을 섞지 않는다. - [ ] REST Gateway가 envelope/cache/idempotency를 자동 제공한다고 가장하지 않는다. - [ ] client/bidi streaming을 browser 공통 capability로 표시하지 않는다. - [ ] actual proxy와 세 browser evidence가 있다. - [ ] command fallback/replay가 backend effect certainty를 우회하지 않는다. - [ ] coherent rollback과 complete removal drill이 통과한다. ## 22. 규범·공식 근거 - [Connect protocol reference](https://connectrpc.com/docs/protocol/) - [Connect-Web protocol selection](https://connectrpc.com/docs/web/choosing-a-protocol/) - [Connect-Web code generation](https://connectrpc.com/docs/web/generating-code/) - [Connect and gRPC-Web CORS](https://connectrpc.com/docs/cors/) - [gRPC-Web protocol delta](https://github.com/grpc/grpc/blob/master/doc/PROTOCOL-WEB.md) - [Official grpc-web runtime](https://github.com/grpc/grpc-web) - [Envoy gRPC-Web filter](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_web_filter.html) - [gRPC HTTP status fallback mapping](https://github.com/grpc/grpc/blob/master/doc/http-grpc-status-mapping.md) - [Envoy timeout configuration](https://www.envoyproxy.io/docs/envoy/latest/faq/configuration/timeouts.html) - [Protocol Buffers language guide](https://protobuf.dev/programming-guides/proto3/) - [ProtoJSON format](https://protobuf.dev/programming-guides/json/) - [Buf breaking changes](https://buf.build/docs/breaking/) - [gRPC-Gateway introduction](https://grpc-ecosystem.github.io/grpc-gateway/docs/tutorials/introduction/) - [gRPC-Gateway customization](https://grpc-ecosystem.github.io/grpc-gateway/docs/mapping/customizing_your_gateway/) - [Google API HTTP annotation](https://github.com/googleapis/googleapis/blob/master/google/api/http.proto) - [Envoy gRPC-JSON transcoder](https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/grpc_json_transcoder_filter)