# VD-29: Connect-Web과 browser Protobuf runtime - 상태: Accepted design — common coordinator available, wire adapter pending - 결정일: 2026-07-28 - provider-neutral Browser RPC V3 coordinator: `AVAILABLE_NOT_COMPOSED` - Connect-Web reference runtime: `DESIGNED_NOT_IMPLEMENTED` - product Connect-Web composition: `NOT_SELECTED` - browser client-streaming/bidirectional-streaming guarantee: `PLATFORM_LIMITED` - 관련 결정: VD-13, VD-23, VD-24, VD-25, VD-27, VD-28, 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가 있다. direct dependency, `.proto`/descriptor, Buf config, generated output, provider route와 actual browser/proxy test에는 Connect-Web capability가 없다. 문서와 future binding이 있다는 사실은 runtime 구현 또는 backend 연결 증거가 아니다. `@connectrpc/connect-web`은 library 이름이고 protocol 이름이 아니다. 같은 browser runtime이 다음 두 wire protocol을 선택적으로 실행할 수 있다. - Connect protocol - gRPC-Web protocol 두 protocol은 service/method와 Protobuf schema 의미를 공유할 수 있지만 HTTP media, framing, status, error와 terminal 규칙은 다르다. application gateway를 공유할 수 있다는 이유로 decoder와 provider profile을 공유하지 않는다. Connect protocol 자체는 unary, client stream, server stream과 bidirectional stream을 정의하지만 browser용 Connect-Web Fetch transport는 unary와 server stream만 제공한다. protocol capability와 selected browser runtime capability를 같다고 보지 않는다. VD-27은 gRPC-Web frame/status state machine을 소유한다. 이 결정은 Connect-Web-specific runtime matrix와 Connect protocol state machine을 소유한다. VD-27의 특정 XHR/base64 runtime 제약을 모든 gRPC-Web runtime에 일반화하지 않고, 선택한 `(protocol, runtime, version, rpc kind, encoding, browser, proxy)` tuple로 capability를 판정한다. ## 결정 ### 1. Protocol, runtime, message encoding과 framing을 분리한다 다음 축은 서로 대체할 수 없다. ```text protocol CONNECT_V1 GRPC_WEB_V1 client runtime CONNECT_ES_WEB_V2_ other explicitly registered runtime rpc kind UNARY SERVER_STREAM message encoding JSON PROTO framing BARE CONNECT_ENVELOPE GRPC_WEB_BINARY_ENVELOPE GRPC_WEB_BASE64_TEXT ``` 금지하는 축약: - `CONNECT_WEB`이라는 값 하나로 protocol과 runtime을 동시에 표현 - `binary`를 Protobuf encoding과 HTTP transfer/framing 모두의 의미로 사용 - `text`를 Protobuf JSON과 `grpc-web-text` base64에 함께 사용 - response `Content-Type`을 보고 client가 protocol/runtime을 자동 선택 - 같은 body를 Connect envelope와 gRPC-Web frame decoder에 차례로 시도 operation은 exact protocol 하나에 binding한다. provider가 같은 procedure path에서 Connect와 gRPC-Web을 모두 제공하더라도 client operation이 runtime negotiation이나 failure fallback을 하지 않는다. ### 2. Connect-Web browser capability matrix 선택한 exact Connect-Web v2 package에 대해 허용할 수 있는 baseline: | protocol | RPC | encoding | method | selected framing | baseline | | --- | --- | --- | --- | --- | --- | | Connect | unary | JSON | POST | bare JSON | 후보 | | Connect | unary | Proto | POST | bare Proto | 후보 | | Connect | unary | JSON | GET | URL query | 제한적 후보 | | Connect | unary | Proto | GET | URL-safe base64 query | 제한적 후보 | | Connect | server stream | JSON | POST | Connect envelope | 후보 | | Connect | server stream | Proto | POST | Connect envelope | 후보 | | gRPC-Web | unary | Proto | POST | binary envelope | 후보 | | gRPC-Web | unary | JSON | POST | binary envelope | provider evidence 필요 | | gRPC-Web | server stream | Proto | POST | binary envelope | actual Fetch/proxy evidence 필요 | | gRPC-Web | server stream | JSON | POST | binary envelope | provider와 actual Fetch/proxy evidence 필요 | Connect-Web의 gRPC-Web transport는 `grpc-web-text`를 구현하지 않는다. 따라서 다음 row는 build/boot 전에 거절한다. ```text CONNECT_ES_WEB_V2_* + GRPC_WEB_BASE64_TEXT ``` Connect-Web Fetch transport에서 다음 row도 거절한다. ```text CLIENT_STREAM BIDIRECTIONAL_STREAM ``` client/bidi가 필요한 use case는 upload/job command, WebSocket, WebTransport 또는 별도 backend aggregation protocol로 다시 선택한다. browser Fetch request body streaming이 일부 환경에 존재한다는 사실만으로 이 platform guarantee를 바꾸지 않는다. ### 3. Application 경계 ```text application port -> registered semantic operation -> exact WebRpcOperationV1 -> generated request construction -> semantic request validation -> selected Connect-Web transport -> protocol-specific bounded response validation -> generated response decode -> semantic runtime validation -> handwritten boundary mapper -> application result or mapped stream event ``` application, domain, presentation과 Query cache에 다음을 노출하지 않는다. - generated service descriptor/client - generated Protobuf message - `ConnectError` - service/method string - raw headers/trailers - Connect envelope 또는 gRPC-Web frame - protobuf `bigint`, `bytes`, enum/oneof representation - transport, base URL, call option와 arbitrary metadata feature는 `client.say(request)` 같은 generated RPC를 직접 호출하지 않는다. `getResource`, `searchResources`, `watchJob`처럼 use-case 의미를 가진 port만 사용한다. ### 4. Versioned operation binding 기존 구현 baseline의 operation registry에는 Connect가 없다. 이 설계의 `ApiOperationContractV3`는 기존 `GRPC_WEB` 값을 재해석하지 않고 `CONNECT_HTTP` discriminant를 추가한다. source registry와 runtime이 V3를 실제 구현하기 전에는 Connect operation을 `COMPOSED`로 판정할 수 없다. ```text WebRpcOperationV1 registryVersion semanticOperationId owner providerId protocol = CONNECT_V1 | GRPC_WEB_V1 protocolRevision clientRuntimeId clientRuntimeIntegrity rpcKind = UNARY | SERVER_STREAM messageEncoding = JSON | PROTO framingProfileId requestMethodProfileId fullyQualifiedService method requestMessageId responseMessageId descriptorArtifactId descriptorDigest idempotencyLevel requestSemanticSchemaId responseSemanticSchemaId mapperId errorProfileId statusMediaProfileId metadataProfileId authProfileId csrfProfileId replayPolicy idempotencyKeyPolicy deadlineProfileId retryProfileId interceptorChainId compressionProfileId serverStateProfileId | null maxRequestMessageBytes maxBrowserVisibleResponseBytes maxEnvelopeBytes maxResponseMessageBytes maxResponseMessages maxTotalResponseBytes maxBufferedBytes idleDeadlineMs totalDeadlineMs dataClassification ``` protocol-specific closed binding: ```text CONNECT_V1 connectProtocolVersion = 1 unaryMode = POST_ONLY | GET_NO_SIDE_EFFECTS connectStatusProfileId connectEndStreamProfileId | null GRPC_WEB_V1 grpcWebWireSpecRevision grpcWebTransportProfileId grpcWebHttpStatusProfileId grpcWebTrailerProfileId ``` registry가 build/boot 전에 거절: - protocol과 media/framing/status binding 불일치 - floating runtime version, missing lock integrity 또는 unknown runtime ID - descriptor/source/generated digest mismatch - unknown service/method/message - generated method kind와 registered `rpcKind` 불일치 - Connect binding을 gRPC-Web decoder에 연결 - Connect-Web runtime의 `grpc-web-text` - Connect-Web runtime의 client/bidi method - unary response count가 1이 아님 - server stream의 message/count/total/buffer/idle/total cap 누락 - caller-provided endpoint/service/method/header - selected runtime/browser/proxy capability matrix에 없는 row - non-replayable command retry - ordinary Query cache가 stream data owner ### 5. Exact provider profile ```text ConnectWebProviderProfileV1 providerId fixedHttpsOrigin pathPrefix clientRuntimeId packageName = @connectrpc/connect-web exactPackageVersion packageLockIntegrity runtimeSourceRevision runtimeConformanceFixtureDigest protocol = CONNECT_V1 | GRPC_WEB_V1 protocolRevision rpcKind messageEncoding framingProfileId requestMethodProfileId statusMediaProfileId compressionProfileId transportInstanceId fetchImplementationId interceptorChainId browserCapabilityProfileId proxyCapabilityProfileId credentialsMode authProfileId csrfProfileId corsProfileId referrerPolicy redirect = ERROR maxRequestMessageBytes maxBrowserVisibleResponseBytes maxEnvelopeBytes maxResponseMessageBytes maxResponseMessages maxTotalResponseBytes maxBufferedBytes maximumDeadlineMs ``` operation과 provider의 다음 tuple은 exact match해야 한다. ```text ( protocol, protocolRevision, clientRuntimeId, exactPackageVersion, rpcKind, messageEncoding, framingProfileId, requestMethodProfileId, statusMediaProfileId, compressionProfileId, browserCapabilityProfileId, proxyCapabilityProfileId ) ``` 권장 profile ID 예: ```text CONNECT_ES_WEB__CONNECT_UNARY_JSON_POST_IDENTITY CONNECT_ES_WEB__CONNECT_UNARY_PROTO_POST_IDENTITY CONNECT_ES_WEB__CONNECT_UNARY_JSON_GET_PUBLIC_HEADERLESS CONNECT_ES_WEB__CONNECT_UNARY_PROTO_GET_PUBLIC_HEADERLESS CONNECT_ES_WEB__CONNECT_SERVER_STREAM_JSON_IDENTITY CONNECT_ES_WEB__CONNECT_SERVER_STREAM_PROTO_IDENTITY CONNECT_ES_WEB__GRPC_WEB_UNARY_PROTO_IDENTITY CONNECT_ES_WEB__GRPC_WEB_UNARY_JSON_IDENTITY CONNECT_ES_WEB__GRPC_WEB_SERVER_STREAM_PROTO_IDENTITY CONNECT_ES_WEB__GRPC_WEB_SERVER_STREAM_JSON_IDENTITY ``` `latest`, major version만 있는 runtime ID와 runtime-independent `GRPC_WEB_BINARY` 같은 profile 이름은 compatibility proof가 아니다. ### 6. Protobuf source와 Connect-ES v2 codegen schema source는 backend와 frontend가 공유하는 contract authority다. ```text authenticated `.proto` or Buf module -> immutable source artifact -> descriptor set -> provenance + digest -> Buf lint -> production baseline breaking comparison -> pinned `protoc-gen-es` -> service descriptors + message schemas -> adapter-private generated output -> semantic validators + mapper ``` Connect-ES v2는 Connect-specific code generator를 요구하지 않는다. reference toolchain 후보: ```text development @bufbuild/buf @bufbuild/protoc-gen-es runtime @bufbuild/protobuf @connectrpc/connect @connectrpc/connect-web ``` 신규 v2 구성에 deprecated Connect generator를 추가하지 않는다. governance: - exact Buf, `protoc-gen-es`, runtime, Node와 TypeScript version pin - package lock integrity와 SBOM - source/descriptor/generated output digest - normal build에서 remote `latest` schema fetch 금지 - authenticated explicit schema update와 reviewable diff - clean checkout regenerate diff 0 - Buf lint/breaking - removed field number와 name reserve - package/service/method full name 안정성 - proto3 presence와 optional 의미 - enum zero/unknown numeric policy - oneof absence/unknown case policy - repeated/map/nesting/bytes ceiling - Timestamp/Duration range와 nanos - int64/uint64 safe projection - N/N-1 descriptor/provider conformance fixture - generated import boundary와 dependency removal test generated service descriptor의 idempotency option은 GET/retry 검증 입력이다. build manifest가 `NO_SIDE_EFFECTS`, `IDEMPOTENT`와 unspecified를 구분하고 handwritten operation row와 교차 검증한다. transport가 unknown Protobuf field를 무시하는 default는 forward compatibility 정책일 수 있지만 semantic proof가 아니다. mapper는 known field만 projection하고 VD-24 validator가 presence, range, enum, oneof와 collection budget을 검증한다. `google.protobuf.Any`와 error detail registry는 allowlisted descriptor만 포함한다. ### 7. Request construction ```text validated application input -> generated request shape -> request semantic validator -> bounded JSON/Proto serialization -> protocol-owned method/path/media -> registry-owned metadata/auth/deadline -> final request invariant -> bounded transport ``` invariant: - origin/path/service/method는 registry가 생성 - application caller가 `Headers`, endpoint와 call options를 받지 않음 - request serialization 전 collection/string/bytes cap - serialization 후 encoded byte cap - `Content-Type`, protocol version과 timeout header는 transport owner만 설정 - credentials가 URL, method, body digest를 바꾸지 않음 - redirects are errors - final URL/origin이 fixed provider와 다르면 response를 읽기 전에 실패 - auth-required operation은 valid credential이 없으면 network call 0회 Connect JSON/Proto와 gRPC-Web JSON/Proto serialization option은 profile에 고정한다. runtime caller가 `jsonOptions`, `binaryOptions` 또는 type registry를 override하지 않는다. ### 8. Connect unary JSON/Proto(binary) POST Connect unary POST는 gRPC-Web frame을 사용하지 않는다. | encoding | request media | success media | body | | --- | --- | --- | --- | | JSON | `application/json` | `application/json` | bare Protobuf JSON | | Proto | `application/proto` | `application/proto` | bare binary Protobuf | request는 `Connect-Protocol-Version: 1`을 포함하는 selected profile을 기본으로 한다. timeout을 server에 전파하면 `Connect-Timeout-Ms`를 사용한다. success: ```text HTTP 200 + exact selected media + bounded exactly one bare response message + semantic validator success + mapper success ``` `204`, empty body, extra JSON/body bytes, wrong media, redirect와 multiple response message 의미는 success가 아니다. error: - non-200 Connect error body는 `application/json`이어야 함 - error body를 별도 작은 byte/depth/detail cap으로 bounded decode - valid error code와 HTTP status mapping을 교차 검증 - missing/malformed body는 exact HTTP-to-Connect mapping profile로 닫음 - success media를 가진 intermediary HTML/JSON을 application DTO로 해석하지 않음 - unary trailing metadata는 `Trailer-*` response header profile로만 수용 HTTP status가 meaningful하다는 사실은 REST adapter 재사용 근거가 아니다. procedure path, Protobuf JSON mapping, error code와 trailing metadata가 다르다. ### 9. Connect unary GET GET은 편의 flag가 아니라 별도 threat/cache/provider profile이다. 필수 조건: - `rpcKind=UNARY` - descriptor idempotency가 정확히 `NO_SIDE_EFFECTS` - application semantics가 `QUERY` - replay policy가 `SAFE` - product owner가 side effect 없음과 representation privacy를 승인 - deterministic request encoding evidence - encoded URL byte ceiling - non-sensitive request - exact browser/proxy/CDN cache policy - redirect 없음 `IDEMPOTENT`는 GET 허용 조건이 아니다. 같은 command를 재실행해도 된다는 의미와 side effect가 없다는 의미를 섞지 않는다. wire query는 runtime이 만들며 caller가 직접 조합하지 않는다. ```text connect=v1 encoding=json | proto message= base64=1 when required compression= ``` Proto 또는 binary/compressed payload는 URL-safe base64 규칙을 따른다. 같은 semantic input이 같은 query encoding을 만들지 actual runtime fixture로 검증한다. URL/history, CDN, reverse proxy, browser telemetry와 access log에 노출할 수 없는 field가 있으면 POST를 사용한다. Connect-Web의 `useHttpGet`은 transport option이고 generated method의 `NO_SIDE_EFFECTS`를 보고 GET으로 바꾼다. GET-enabled transport를 검토되지 않은 service/method와 공유하지 않는다. ```text ConnectGetTransportProfile transportInstanceId allowedSemanticOperationIds[] allowedDescriptorMethods[] deterministicEncodingFixtureDigest maximumUrlBytes cacheProfileId headerProfileId ``` baseline `PUBLIC_HEADERLESS` GET: - Authorization, CSRF, trace와 application header 없음 - cookie/private credential 없음 - `Connect-Timeout-Ms` 없음 - local AbortSignal deadline만 사용 - exact `Access-Control-Allow-Origin` - public cacheability와 response classification 일치 timeout/auth header가 필요하면 GET 자체는 가능해도 “headerless/no-preflight” profile이 아니다. `GET_WITH_PREFLIGHT_PRIVATE`를 별도 승인하지 않은 한 POST를 사용한다. HTTP/CDN cache hit도 response validator와 mapper를 건너뛰지 않는다. HTTP cache, TanStack Query memory cache와 persisted cache는 서로 다른 owner다. ### 10. Connect server-stream state machine Connect server stream: ```text POST Content-Type: application/connect+json | application/connect+proto request body: exactly one non-terminal request envelope HTTP success status: exactly 200 response body: one or more Connect envelopes final envelope: exactly one EndStreamResponse ``` browser `SERVER_STREAM` method의 request는 generated input message 정확히 하나다. zero/multiple request message, request-side EndStream flag와 compressed/reserved flag를 거절한다. stream HTTP 200은 application success가 아니다. identity baseline envelope: ```text 1 byte flags 4 byte unsigned big-endian message length N bytes message ``` selected revision에서: - bit 0: compressed message - bit 1: `EndStreamResponse` - 나머지 bit: reserved reference identity profile은 compressed bit와 reserved bit를 거절한다. data envelope는 selected JSON/Proto response message이고 `EndStreamResponse`는 stream encoding과 무관하게 bounded JSON contract다. state: ```text INIT -> HEADERS_ACCEPTED -> DATA* -> END_STREAM_SUCCESS -> END_STREAM_ERROR -> CLOSED ``` terminal invariant: - `EndStreamResponse`가 정확히 한 번 존재 - terminal은 마지막 envelope - terminal 뒤 byte/data/envelope 없음 - success terminal은 `error` property 없음 - failure terminal은 valid non-empty error code - `error: null`, empty error와 unknown code 거절 - trailing metadata key/value/count/decoded-byte cap - error detail type/count/value/decoded-byte cap - EOF 전에 terminal이 없으면 contract failure - duplicate/early terminal은 contract failure incremental decoder: ```text ReadableStream -> bounded 5-byte prefix -> validate flags -> validate declared length before allocation -> bounded envelope payload -> data or EndStream decoder -> semantic message validator -> mapper -> bounded application queue ``` `Content-Length`와 declared envelope length만 믿지 않는다. 다음 budget을 각각 enforce한다. - maximum browser-visible total bytes - maximum envelope bytes - maximum decoded response message bytes - maximum message count - maximum terminal/error JSON bytes - maximum buffered mapped events - idle deadline - total deadline consumer가 queue ceiling보다 느리면 reader backpressure를 유지한다. 계속 증가하는 buffer, UI event마다 무제한 render와 stream 전체 `Uint8Array` materialization을 금지한다. stream에서 이미 전달한 event는 뒤의 terminal error로 rollback되지 않는다. application은 typed terminal failure를 받고 reducer/resume 정책을 수행한다. partial stream을 Query success value로 cache하지 않는다. ### 11. Connect-Web의 gRPC-Web transport `createGrpcWebTransport()`를 선택하면 VD-27의 gRPC-Web frame, trailer와 status state machine을 적용한다. Connect envelope와 `EndStreamResponse`를 적용하지 않는다. Connect-Web runtime-specific 차이: - Fetch 기반 unary/server-stream - binary Protobuf가 default - Protobuf JSON은 provider가 지원할 때 별도 profile - `grpc-web-text` 미지원 - binary server stream은 exact browser/proxy incremental evidence가 있을 때 후보 따라서 VD-27의 다른 runtime용 base64 text row를 삭제하거나 재해석하지 않고 runtime ID별 capability matrix를 만든다. ```text GOOGLE_GRPC_WEB_XHR_ + its exact supported profiles CONNECT_ES_WEB_ + binary-envelope profiles only ``` gRPC-Web binary server stream을 Connect-Web에서 선택하려면 Chromium, Firefox, WebKit과 actual provider/proxy에서 fragmentation, flush, cancellation, backpressure, trailer와 terminal status를 증명해야 한다. ### 12. Stock Connect-Web runtime 한계 stock runtime dependency 설치와 generated client call 성공만으로 production adapter를 `COMPLETE`로 판정하지 않는다. 선택한 runtime source와 package conformance에서 확인할 항목: - unary success가 `Response.json()` 또는 `Response.arrayBuffer()`로 전체 body를 materialize하는지 - unary error JSON도 bounded read 없이 materialize하는지 - envelope parser에 registry-owned envelope/message/total cap hook이 있는지 - compressed output flag 처리 - terminal exact-once/final enforcement - reserved flag 처리 - abort와 reader cleanup - `timeoutMs <= 0` 의미 현재 reference 대상 Connect-Web transport API만으로 registry의 raw-byte, per-envelope와 per-message ceiling을 모두 전달할 수 있다고 가정하지 않는다. reference implementation에는 다음 중 하나가 필요하다. 1. stock client serialization/service descriptor를 사용하고 platform-owned bounded `Transport`가 Fetch, media/status와 decode state를 소유 2. stock transport 앞의 bounded Fetch wrapper와 별도 strict stream conformance layer가 모든 ceiling/terminal invariant를 실제로 증명 3. 필요한 cap hook이 있는 검증된 upstream version을 exact pin bounded Fetch/Transport 요구: - network body가 browser에 노출하는 decoded byte를 incremental count - cap 초과 즉시 reader cancel과 typed contract failure - success와 error body 모두 같은 ceiling owner 통과 - final URL/origin, status와 media를 decode 전 검증 - timeout/AbortSignal을 fetch와 reader에 전달 - response/reader lock을 모든 terminal path에서 release - interceptor 이후가 아니라 wire decode 전에 ceiling 적용 Fetch가 content decoding을 끝낸 뒤 제공하는 byte만 client에서 셀 수 있는 경우, raw compressed byte와 decompression ratio ceiling은 BFF/proxy가 소유한다. client cap을 upstream cap의 대체물로 간주하지 않는다. selected exact package version의 malformed terminal fixture가 protocol의 exact-once/final 요구를 만족하지 못하면 wrapper가 아니라 custom bounded Transport/parser로 닫는다. 이 gap이 닫히기 전 status: ```text DESIGNED_NOT_IMPLEMENTED ``` ### 13. Compression profile Connect protocol은 unary HTTP compression과 streaming message compression을 정의한다. protocol capability가 stock browser runtime implementation 증거는 아니다. reference baseline: ```text requestMessageCompression = IDENTITY streamResponseMessageCompression = IDENTITY connectGetCompression = IDENTITY ``` Connect-Web stream decoder가 compressed output을 지원하지 않는 selected version은 compressed flag를 contract failure로 처리한다. proxy가 임의로 message compression을 활성화하지 않도록 upstream configuration과 fixture를 둔다. unary whole-body HTTP `Content-Encoding`은 별도 profile이다. ```text UnaryHttpCompressionProfile mode = IDENTITY_ONLY | BROWSER_MANAGED allowedCodings[] maximumEncodedBytesAtProxy maximumDecodedBytesAtProxy maximumBrowserVisibleDecodedBytes ``` `BROWSER_MANAGED`는 actual browser가 decode한 body에 client ceiling을 적용하고, BFF/proxy가 encoded/decompressed ceiling과 decompression bomb 방어를 증명할 때만 승인한다. 첫 reference profile은 `IDENTITY_ONLY`다. ### 14. Interceptor chain Connect interceptor는 onion 구조이고 configuration array의 마지막 interceptor가 먼저 적용된다. source array 순서를 읽고 security order를 추측하지 않도록 effective execution order를 registry artifact와 test evidence로 만든다. 권장 logical order: ```text total deadline owner -> logical-call telemetry -> exactly-one replay/retry coordinator -> attempt telemetry -> credential owner -> final request invariant -> bounded transport ``` ```text ConnectInterceptorChainV1 interceptorChainId effectiveOrder[] implementationDigests[] allowedContextKeyIds[] retryOwner credentialOwner finalMutatorId ``` invariant: - deadline owner는 retry/backoff 전체를 감쌈 - retry coordinator가 attempt마다 남은 deadline을 계산 - credential attach 뒤 URL/method/body를 바꾸는 interceptor 없음 - final invariant 뒤 mutating interceptor 없음 - retry/auth-refresh/Query/proxy가 같은 call을 중복 replay하지 않음 - streaming response 관찰은 `AsyncIterable`을 bounded wrapper로 감쌈 - stream을 logging 목적으로 선소비하거나 tee하여 무제한 buffer하지 않음 - application caller에게 arbitrary `ContextValues`를 노출하지 않음 - context key는 typed, collision-free, registry-owned control 값만 사용 interceptor logging은 raw request/response, URL query message, metadata, Protobuf payload와 backend error message를 기록하지 않는다. ### 15. Error boundary Connect-Web은 supported protocol의 실패를 `ConnectError`로 표면화할 수 있지만 application error type으로 사용하지 않는다. ```text bounded protocol failure -> ConnectError inspection inside adapter -> registered error profile -> safe AppFailure ``` safe mapping 입력: - protocol - registered operation/provider/runtime/profile ID - allowlisted Connect/gRPC code - local cancel/deadline/retry context - allowlisted typed error detail projection 기본적으로 폐기 또는 redaction: - `message`와 `rawMessage` - arbitrary metadata/header/trailer - error detail raw bytes - error detail `debug` - field values, resource identifiers와 provider stack ```text WebRpcErrorProfileV1 errorProfileId allowedCodes[] codeToAppFailure[] allowedDetailTypes[] maximumErrorBodyBytes maximumErrorDepth maximumDetailCount maximumDetailValueBytes maximumTotalDetailBytes metadataAllowlist[] ``` Connect unary는 HTTP status와 explicit error code를 교차 검증한다. malformed/missing error body의 HTTP mapping과 valid explicit error mapping을 구분한다. Connect stream은 final `EndStreamResponse.error`가 authority다. gRPC-Web은 VD-27의 terminal status/trailer authority를 사용한다. 같은 `ConnectError` API가 세 wire status source를 같게 만들지 않는다. error code만으로 retry하지 않는다. `Unavailable`은 operation replay safety와 backend effect 여부를 증명하지 않는다. ### 16. Total deadline, timeout과 cancellation deadline은 다음 전체를 포함한다. ```text auth readiness + request validation/serialization + network attempts + retry backoff + response read + generated decode + semantic validation + mapper + stream idle/total lifetime ``` 각 attempt 직전: ```text remaining = deadlineAt - monotonicNow if remaining <= 0: fail locally, fetch 0회 else: pass remaining as timeoutMs ``` stock runtime에서 `timeoutMs <= 0`이 “timeout 없음”으로 해석될 수 있으므로 0을 전달해 즉시 timeout을 기대하지 않는다. wire timeout: - Connect: `Connect-Timeout-Ms` - gRPC-Web: `Grpc-Timeout` local timer와 AbortSignal도 유지한다. server timeout header만으로 browser reader와 mapper가 정지한다고 가정하지 않는다. cancel cause를 별도로 분류한다. ```text USER NAVIGATION SCOPE_CHANGE SESSION_CHANGE DEADLINE KILL_SWITCH CONSUMER_CLOSED ``` Connect-Web이 AbortSignal failure를 `ConnectError(Code.Canceled)`로 정규화해도 local cause를 잃지 않는다. cancel 시 fetch, stream reader, mapper queue와 downstream subscription을 함께 닫는다. command abort는 backend effect가 없다는 증거가 아니다. keyed command는 같은 idempotency key로 reconcile하고 non-replayable command는 outcome unknown을 표시한다. ### 17. Retry와 stream resume 한 logical operation의 retry owner는 정확히 하나다. | semantics | baseline | | --- | --- | | safe unary query | bounded retry 후보 | | idempotent unary | descriptor + backend evidence가 있을 때 후보 | | keyed command | backend dedupe/reconcile 뒤 같은 key로만 후보 | | non-replayable command | retry 금지 | | server stream open | 첫 mapped message 전 safe/idempotent row만 후보 | | server stream after message | transport retry 금지 | retry decision은 다음 conjunction이다. ```text operation replay policy AND descriptor idempotency AND provider evidence AND failure category AND remaining deadline/backoff budget AND attempt ceiling ``` Connect code의 일반 설명만으로 command를 replay하지 않는다. 401 refresh replay, interceptor retry, TanStack Query retry와 proxy retry를 별개 owner로 동시에 켜지 않는다. stream에서 한 message라도 application에 전달한 뒤 reconnect는 새로운 transport attempt가 아니라 application resume protocol이다. ```text sequence resume token dedupe window gap policy snapshot/resync retention reauth ``` 이 계약이 backend에 없으면 stream failure 뒤 자동 reconnect하지 않는다. ### 18. CORS, credentials, auth와 CSRF same-origin BFF를 기본 권장한다. cross-origin provider는 actual origin에서 exact CORS evidence가 필요하다. gRPC-Web baseline: ```text Access-Control-Allow-Methods: POST Access-Control-Allow-Headers: Content-Type, Grpc-Timeout, X-Grpc-Web, X-User-Agent Access-Control-Expose-Headers: Grpc-Status, Grpc-Message, Grpc-Status-Details-Bin ``` Connect baseline: ```text Access-Control-Allow-Methods: GET, POST Access-Control-Allow-Headers: Content-Type, Connect-Protocol-Version, Connect-Timeout-Ms, X-User-Agent ``` Authorization, CSRF, idempotency와 trace header가 있으면 exact allowlist에 추가한다. custom response header는 expose하고 Connect unary custom trailer는 `Trailer-`을 expose한다. dynamic preflight response: ```text Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers ``` preflight cache TTL은 rollback propagation budget 안에 둔다. cached permissive CORS가 incident response보다 오래 남지 않게 한다. credentials: - wildcard origin과 credentials 조합 금지 - fixed allowed origin - exact `credentialsMode` - TLS와 redirect 없음 - credential owner 외 caller header 금지 - token/cookie를 GET query message에 넣지 않음 Connect GET이 preflight를 피하려면 timeout을 포함한 request header가 없어야 한다. response는 origin을 허용해야 한다. header가 추가되면 normal preflight profile로 판정한다. cookie profile은 protocol이 POST 또는 non-simple media라는 이유만으로 CSRF가 해결됐다고 보지 않는다. backend가 Origin, Fetch Metadata와 selected CSRF token contract를 검증한다. baseline public GET에는 cookie credential을 사용하지 않는다. ### 19. Provider, BFF와 proxy evidence Connect-native server/BFF는 content type으로 같은 procedure path의 Connect와 gRPC-Web을 구분해 제공할 수 있다. ```text Connect unary: application/json application/proto Connect stream: application/connect+json application/connect+proto gRPC-Web: application/grpc-web application/grpc-web+proto application/grpc-web+json ``` client는 이 content type을 protocol negotiation에 사용하지 않고 selected provider row의 response 검증에만 사용한다. Envoy `grpc_web` filter가 존재한다는 사실은 Connect protocol endpoint 증거가 아니다. Connect protocol을 선택하려면 Connect-aware BFF/server/gateway의 exact version, route와 fixture가 필요하다. ```text ProxyCapabilityProfileV1 proxyProduct exactVersion configDigest downstreamProtocol upstreamProtocol routeDigest corsProfileId authProfileId requestLimitProfileId responseLimitProfileId timeoutPropagationProfileId streamBuffering = DISABLED streamFlushProfileId statusPreservationProfileId terminalPreservationProfileId ``` staging/production-like evidence: - exact service/method routing - request/response content type preservation - protocol version/timeout/auth header behavior - encoded/decompressed/message/body ceiling - Connect unary HTTP/error mapping - Connect stream HTTP 200 + final EndStream preservation - gRPC-Web body trailer/status preservation - no gateway status synthesis without selected fixture - stream first-byte and inter-message flush - no proxy/CDN antivirus full-body buffering - AbortSignal/downstream disconnect propagation - slow consumer/backpressure behavior - idle/total timeout behavior - Chromium, Firefox와 WebKit - supported HTTP/1.1/HTTP/2 and enterprise path fake Fetch와 unit mock만으로 provider conformant가 아니다. ### 20. Runtime schema와 mapper generated Protobuf decode success 뒤에도 semantic validation을 수행한다. ```text untrusted bounded bytes/message -> generated JSON/Proto decode -> operation-specific RuntimeCodec -> ValidatedDto -> BoundaryMapper -> immutable application projection ``` validator/mapper가 소유: - required/presence meaning - string/list/map/bytes semantic ceiling - int64/uint64 safe range 또는 decimal projection - Timestamp/Duration normalization - enum zero와 unknown numeric mapping - oneof absence/unknown branch - NaN/Infinity and numeric domain rule - field-level data classification - unknown-field projection - error detail allowlist mapper는 throw-only 함수가 아니라 typed mapping result를 반환한다. generated message, native `Uint8Array`, class instance와 transport metadata가 output에 남으면 cache admission과 application boundary를 거절한다. server stream은 message마다 validate/map한 뒤 bounded reducer로 넘긴다. raw Protobuf message를 realtime JSON envelope로 다시 감싸지 않는다. ### 21. Server State와 Connect-Query TanStack Query가 mapped unary server state의 유일한 기본 memory owner다. ```text Connect/gRPC-Web unary -> bounded protocol success -> semantic validation -> mapper -> immutable application read model -> TanStack Query ``` Query key는 semantic identity를 사용한다. 금지: - service/method string - Protobuf request bytes/generated message - Connect GET URL/query string - protocol name/runtime version - raw metadata/trailer - `ConnectError` transport가 REST에서 Connect 또는 gRPC-Web으로 바뀌어도 use-case/result 의미가 같으면 semantic query family를 유지할 수 있다. mapper/result identity나 scope가 바뀌면 query definition version/release epoch를 바꾼다. `@connectrpc/connect-query`는 reference default dependency가 아니다. generated operation을 presentation에서 직접 호출하거나 generated message를 Query cache에 넣어 기존 gateway/schema/mapper/key policy를 우회하지 않는다. 실제 도입하려면 별도 ADR이 같은 boundary와 single cache owner를 증명해야 한다. Connect GET의 browser/CDN cache와 TanStack Query는 별도 owner다. HTTP cache response도 adapter validation/mapper를 통과하고, private scope를 public cache에서 복원하지 않는다. command result를 query data owner로 쓰지 않는다. success 뒤 registered invalidation topic/version을 발행한다. stream partial state는 ordinary Query cache가 아니라 bounded feature reducer/snapshot owner가 관리한다. transport retry가 enabled인 query는 TanStack `retry=false`다. ### 22. Observability와 privacy low-cardinality dimensions: ```text semanticOperationId providerId protocol clientRuntimeId rpcKind messageEncoding framingProfileId result category retry bucket cancel cause browserCapabilityProfileId proxyCapabilityProfileId ``` metrics: - logical calls/attempts/retries - auth wait - first-byte latency - unary total latency - stream open/active duration - message count/bytes/buffer high-water mark - terminal success/error/missing/early/duplicate - media/status mismatch - byte/envelope/message cap rejection - deadline/cancel cause - CORS/preflight/provider incompatibility - mapper/schema rejection 기록하지 않음: - request/response Protobuf or JSON - Connect GET `message` query - auth/CSRF/idempotency token - metadata/trailer raw value - backend error message/detail/debug - user/resource identifier - generated descriptor payload trace propagation은 interceptor-owned allowlisted header만 사용한다. arbitrary provider metadata를 trace attribute로 복사하지 않는다. ### 23. Security invariants - fixed HTTPS origin/path prefix - caller-controlled absolute URL 없음 - redirects rejected - service/method from immutable descriptor/registry only - arbitrary header/context value 없음 - auth/CSRF owner 분리 - request/response/error/metadata/stream queue cap - decompression cap - no raw payload logging - GET sensitive input 금지 - application-level authorization은 backend authority - frontend schema validation을 authorization으로 간주하지 않음 - stream event는 현재 session/scope generation과 일치할 때만 적용 - kill switch가 신규 call/stream admission을 막고 active reader를 cancel 같은 origin에 Connect와 gRPC-Web route가 있어도 content-type confusion fixture를 실행한다. Connect body를 REST JSON endpoint가 받아들이거나 gRPC-Web frame을 Connect stream으로 해석하는 route를 허용하지 않는다. ### 24. Verification matrix registry/build: - duplicate/unknown profile와 descriptor 거절 - runtime/package integrity mismatch 거절 - protocol/framing/media/status tuple mismatch 거절 - Connect-Web + `grpc-web-text` 거절 - client/bidi 거절 - GET without `NO_SIDE_EFFECTS` 거절 - missing cap/deadline/replay policy 거절 - generated import boundary - clean code generation, lint/breaking와 digest Connect unary: - JSON/Proto success - wrong media, empty/extra body, 204와 redirect - bounded malformed success body - all allowlisted explicit error codes - malformed/missing/oversized error body - HTTP/error code mismatch - prefixed trailer allowlist/cap - browser-visible byte cap Connect GET: - only `NO_SIDE_EFFECTS` - deterministic JSON/Proto encoding - URL-safe base64 - URL ceiling - sensitive field rejection - headerless/no-preflight profile - timeout/auth header가 있는 preflight profile rejection - cache key/Vary/private/public behavior Connect server stream: - prefix가 모든 byte 경계에서 fragmentation - declared length 0, boundary와 cap 초과 - truncated prefix/payload - compressed/reserved flag rejection - JSON/Proto message decode failure - zero/many messages followed by valid terminal - missing, duplicate, early terminal - data/bytes after terminal - malformed/oversized terminal JSON - terminal error/details/metadata cap - slow consumer/backpressure - idle/total deadline와 cancellation Connect-Web gRPC-Web: - binary Proto unary/server stream - approved JSON provider row - `grpc-web-text` rejection - VD-27 frame/trailer/status negative corpus - actual Fetch incremental behavior retry/cancel: - one retry owner - total attempt/backoff/deadline ceiling - `timeoutMs <= 0` network call 0회 - AbortSignal propagates to reader - no replay for non-replayable command - keyed command same key/reconcile - stream first message 이후 reconnect 0회 CORS/proxy/browser: - exact preflight allow/expose/Vary - credential/wildcard rejection - `Trailer-*` exposure - content type and terminal preservation - no stream buffering - Chromium/Firefox/WebKit with actual provider/proxy cache/mapper: - generated/raw values admission rejection - semantic key only - scope/session generation switch - protocol migration without duplicate server-state owner - stream partial state Query cache rejection malformed/oversized fixture는 OOM, unhandled rejection과 secret-bearing log 없이 fail-closed해야 한다. ### 25. Capability state와 promotion 현재: ```text design = ACCEPTED referenceRuntime = DESIGNED_NOT_IMPLEMENTED productComposition = NOT_SELECTED clientOrBidi = PLATFORM_LIMITED ``` state transition: ```text DESIGNED_NOT_IMPLEMENTED -> AVAILABLE_NOT_COMPOSED -> COMPOSED / TrafficAdmission=DISABLED -> INTERNAL_CANARY -> LIMITED -> ACTIVE ``` `AVAILABLE_NOT_COMPOSED` 조건: - exact dependencies와 generated artifacts 존재 - versioned registry and runtime profiles 구현 - bounded unary/stream transport 구현 - schema/mapper/error/deadline/retry/cache boundary 구현 - negative/conformance corpus 통과 - dependency/removal test 통과 `COMPOSED` 조건: - product operation/provider/backend owner 선택 - proto/descriptor and N/N-1 contract - auth/CSRF/CORS/cache/retry/resume contract - actual provider/proxy/browser evidence - runtime config/kill switch/traffic admission - observability and incident owner 문서만으로 state를 올리지 않는다. ### 26. Rollout 1. product가 bounded-context operation family와 backend/provider owner를 선택한다. 2. operation마다 Connect 또는 gRPC-Web protocol 하나를 선택한다. 3. immutable proto/descriptor, idempotency와 compatibility baseline을 확정한다. 4. exact Connect-Web/codegen/runtime dependency를 optional adapter boundary에 설치한다. 5. bounded unary와 stream transport, validator/mapper/error profile을 구현한다. 6. fake provider negative corpus와 exact package conformance를 통과한다. 7. actual BFF/proxy/browser matrix를 통과한다. 8. reference runtime을 `AVAILABLE_NOT_COMPOSED`로 판정한다. 9. product composition 뒤 `TrafficAdmission=DISABLED`로 배포한다. 10. safe unary read를 shadow 비교하되 secondary cache/UI write를 금지한다. 11. internal canary에서 단일 protocol만 admission한다. 12. server stream은 unary와 별도 canary/evidence로 승격한다. 13. keyed command는 backend dedupe/reconcile 뒤 별도 canary한다. 14. client/bidi는 계속 `PLATFORM_LIMITED`로 표시한다. shadow/fallback: - Connect 결과와 REST 결과를 비교해도 cache/UI owner는 primary 하나 - client가 Connect failure를 REST/gRPC-Web로 자동 재실행하지 않음 - command dual-write 금지 - protocol 전환은 새 logical operation/controlled rollout rollback: - 신규 operation/stream traffic admission 중지 - active reader와 pending call cancel - ambiguous command reconcile - affected mapped Query cache clear/invalidate - frontend/generated descriptor/provider route의 coherent rollback - preflight/CDN cache TTL 고려 - approved previous protocol은 별도 registered operation으로만 복구 ### 27. Incident와 recovery kill switch granularity: ```text provider protocol runtime version operation rpc kind message encoding GET profile server stream ``` incident triage 순서: 1. semantic operation/provider/runtime/profile 확인 2. admission과 retry 차단 3. active stream cancel 4. command outcome/dedupe reconciliation 5. browser/proxy/provider/backend change correlation 6. media/status/terminal/cap/deadline bucket 확인 7. raw payload 없이 approved fixture로 재현 8. coherent rollback 또는 provider quarantine missing terminal, wrong media와 mapper rejection을 generic network error로 숨기지 않는다. 사용자는 safe application failure만 보지만 operations telemetry는 contract/provider category를 구분한다. ### 28. Removal 1. operation traffic admission 중지와 usage 확인 2. active stream/call cancel, command reconcile 3. mapped Query cache/reducer/invalidation listener clear 4. operation/provider/schema/mapper/error/cache profile 제거 5. generated files와 descriptor/source artifact reference 제거 6. Buf/codegen/Connect runtime direct dependency 제거 7. route/CORS/proxy/CDN config 제거 또는 다른 client owner에게 이관 8. deprecated proto field reserve와 backend N/N-1 retirement window 준수 9. lockfile/SBOM/license/manifest에서 dependency 제거 확인 10. production bundle/module graph와 removal test 통과 같은 provider route가 다른 native/mobile/backend client를 지원하면 frontend 제거가 provider route 삭제 권한을 뜻하지 않는다. owner와 traffic evidence를 확인한다. ## 금지 조합 요약 - Connect protocol을 `GRPC_WEB` registry row로 가장 - Connect와 gRPC-Web frame/status decoder 공유 - Connect-Web + `grpc-web-text` - browser Connect-Web client/bidi - protocol auto-detection/fallback - GET without exact `NO_SIDE_EFFECTS` - sensitive/authenticated baseline headerless GET - shared unreviewed `useHttpGet=true` transport - compressed stream with unsupported runtime - unbounded `Response.json()`/`arrayBuffer()`를 production cap으로 간주 - `timeoutMs=0`을 immediate timeout으로 사용 - arbitrary interceptor/header/context/endpoint - duplicate transport/Query/proxy retry - raw `ConnectError`/metadata/detail/debug 노출 - generated client/message in presentation, application or Query cache - Connect-Query direct page usage - generic Envoy gRPC-Web filter를 Connect endpoint 증거로 사용 - fake Fetch만으로 provider/browser conformance 판정 - partial stream을 ordinary Query success로 cache - command protocol failover/dual-write ## 규범 기준 - [Connect protocol reference](https://connectrpc.com/docs/protocol/) - [Choosing Connect or gRPC-Web](https://connectrpc.com/docs/web/choosing-a-protocol/) - [Using Connect-Web clients](https://connectrpc.com/docs/web/using-clients/) - [Connect GET requests and caching](https://connectrpc.com/docs/web/get-requests-and-caching/) - [Connect-Web interceptors](https://connectrpc.com/docs/web/interceptors/) - [Connect-Web errors](https://connectrpc.com/docs/web/errors/) - [Connect-Web cancellation and timeouts](https://connectrpc.com/docs/web/cancellation-and-timeouts/) - [Connect and gRPC-Web CORS](https://connectrpc.com/docs/cors/) - [Connect multi-protocol support](https://connectrpc.com/docs/multi-protocol/) - [Connect-Web code generation](https://connectrpc.com/docs/web/generating-code/) - [Connect-ES v2 migration](https://connectrpc.com/docs/web/migrating-to-v2/) - [Connect-Web Connect transport source](https://github.com/connectrpc/connect-es/blob/main/packages/connect-web/src/connect-transport.ts) - [Connect-Web gRPC-Web transport source](https://github.com/connectrpc/connect-es/blob/main/packages/connect-web/src/grpc-web-transport.ts) ## 완료 기준 - Connect와 gRPC-Web이 separate protocol binding/status/framing profile이다. - exact runtime package version/integrity/source와 provider tuple이 고정된다. - Connect-Web runtime의 gRPC-Web text와 client/bidi 조합이 거절된다. - Connect unary JSON/Proto POST가 bounded status/media/error state를 가진다. - GET이 exact `NO_SIDE_EFFECTS`, privacy, deterministic encoding, URL/CORS/cache profile을 모두 만족한다. - Connect server stream이 bounded envelope/message/queue와 exact final `EndStreamResponse`를 검증한다. - stock runtime의 unary raw-byte와 stream compression/terminal gap이 bounded Fetch/Transport 또는 검증된 upstream hook으로 닫힌다. - total deadline, positive remaining timeout, cancellation과 exactly-one retry owner가 증명된다. - CORS/auth/CSRF/proxy/browser evidence가 exact provider에서 유효하다. - `protoc-gen-es` v2 generation, descriptor provenance/breaking/digest가 재현 가능하다. - generated message/client/ConnectError가 adapter 밖으로 나오지 않는다. - mapped immutable unary result만 semantic key로 TanStack Query에 admission된다. - stream partial state와 raw metadata가 Query cache에 없다. - actual Chromium/Firefox/WebKit과 proxy conformance가 통과한다. - kill switch, coherent rollback, incident recovery와 dependency/generated/provider removal drill이 통과한다. - product operation/provider가 선택되기 전 `NOT_SELECTED`를 유지한다.