Files
clean-architecture-frontend…/docs/architecture/decisions/VD-29-connect-web-and-browser-protobuf-runtime.md
T

1474 lines
47 KiB
Markdown

# 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_<exact-package-version>
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_<version>_CONNECT_UNARY_JSON_POST_IDENTITY
CONNECT_ES_WEB_<version>_CONNECT_UNARY_PROTO_POST_IDENTITY
CONNECT_ES_WEB_<version>_CONNECT_UNARY_JSON_GET_PUBLIC_HEADERLESS
CONNECT_ES_WEB_<version>_CONNECT_UNARY_PROTO_GET_PUBLIC_HEADERLESS
CONNECT_ES_WEB_<version>_CONNECT_SERVER_STREAM_JSON_IDENTITY
CONNECT_ES_WEB_<version>_CONNECT_SERVER_STREAM_PROTO_IDENTITY
CONNECT_ES_WEB_<version>_GRPC_WEB_UNARY_PROTO_IDENTITY
CONNECT_ES_WEB_<version>_GRPC_WEB_UNARY_JSON_IDENTITY
CONNECT_ES_WEB_<version>_GRPC_WEB_SERVER_STREAM_PROTO_IDENTITY
CONNECT_ES_WEB_<version>_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=<percent-encoded or URL-safe base64 payload>
base64=1 when required
compression=<selected profile only>
```
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_<version> + its exact supported profiles
CONNECT_ES_WEB_<version> + 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-<name>`을 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`를 유지한다.