|
33 | 33 | max_concurrent_streams: {{= it.platform.gateway.listeners.dapiAndDrive.http2.maxConcurrentStreams }} |
34 | 34 | # Stream idle timeout |
35 | 35 | stream_idle_timeout: 15s |
| 36 | +{{? it.platform.gateway.rateLimiter.enabled }} |
| 37 | + # Make the grpc-web over-limit reply readable as a rate-limit BAN by browser |
| 38 | + # (grpc-web/wasm) clients. The native gRPC over-limit local reply is HTTP 200 |
| 39 | + # + grpc-status:8 (trailers-only) because Envoy detects content-type |
| 40 | + # application/grpc; the grpc-web request (application/grpc-web+proto) is NOT |
| 41 | + # gRPC-detected, so `rate_limited_as_resource_exhausted` never tags it and the |
| 42 | + # browser sees a bare HTTP 429 with no grpc-status -> tonic-web maps it to |
| 43 | + # Unavailable, not ResourceExhausted, so rate_limit_ban_duration() returns None. |
| 44 | + # |
| 45 | + # This mapper rewrites ONLY the grpc-web over-limit local reply (status 429 AND |
| 46 | + # request carried `x-grpc-web`, which the native and JSON-RPC paths never send) |
| 47 | + # to status 200 + grpc-status:8. Because the reply is headers-only (empty body, |
| 48 | + # end_stream on headers) the grpc_web encoder passes it through untouched |
| 49 | + # (encodeHeaders returns Continue when end_stream=true), so grpc-status:8 and |
| 50 | + # the existing ratelimit-reset header stay CO-LOCATED in the HTTP response |
| 51 | + # headers. tonic-web's create_response then builds the Status (and its metadata, |
| 52 | + # incl. ratelimit-reset) from those headers via Status::from_header_map without |
| 53 | + # reading the body, giving browser clients the same ResourceExhausted + |
| 54 | + # ratelimit-reset node-backoff the native client gets. CORS expose_headers |
| 55 | + # already lists both grpc-status and ratelimit-reset (see virtual host below). |
| 56 | + local_reply_config: |
| 57 | + mappers: |
| 58 | + - filter: |
| 59 | + and_filter: |
| 60 | + filters: |
| 61 | + - status_code_filter: |
| 62 | + comparison: |
| 63 | + op: EQ |
| 64 | + value: |
| 65 | + default_value: 429 |
| 66 | + runtime_key: ratelimit_grpc_web_local_reply_status |
| 67 | + - header_filter: |
| 68 | + header: |
| 69 | + name: x-grpc-web |
| 70 | + present_match: true |
| 71 | + status_code: 200 |
| 72 | + headers_to_add: |
| 73 | + - header: |
| 74 | + key: grpc-status |
| 75 | + value: "8" |
| 76 | + append_action: OVERWRITE_IF_EXISTS_OR_ADD |
| 77 | + - header: |
| 78 | + key: grpc-message |
| 79 | + value: "rate limited" |
| 80 | + append_action: OVERWRITE_IF_EXISTS_OR_ADD |
| 81 | +{{?}} |
36 | 82 | {{? it.platform.gateway.log.accessLogs }} |
37 | 83 | access_log: |
38 | 84 | {{~ it.platform.gateway.log.accessLogs :log }} |
|
78 | 124 | {{?}} |
79 | 125 | http_filters: |
80 | 126 | # TODO: Introduce when stable https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/adaptive_concurrency_filter.html |
| 127 | + # Filter order matters: cors and grpc_web MUST precede ratelimit so the |
| 128 | + # over-limit RESOURCE_EXHAUSTED *local reply* gets CORS headers + grpc-web |
| 129 | + # content-type framing on encode, letting browser (grpc-web) clients read |
| 130 | + # RateLimit-Reset for the same node-backoff the native client does. A local |
| 131 | + # reply only traverses encoder filters positioned ABOVE its generating |
| 132 | + # filter (Envoy #11776), so ratelimit must come last (before router). |
| 133 | + # Live-verified on the pinned Envoy build (PR #3951): the grpc-web over-limit |
| 134 | + # reply is trailers-only (HTTP 200 + grpc-status:8 + ratelimit-reset as HTTP |
| 135 | + # headers, empty body — grpc_web leaves it untouched, no body trailer frame). |
| 136 | + # The grpc-status:8 itself is synthesized by the local_reply_config mapper |
| 137 | + # above (Envoy's gRPC detection ignores application/grpc-web), giving browser |
| 138 | + # clients the same ResourceExhausted + ratelimit-reset node-backoff as native. |
| 139 | + - name: envoy.filters.http.cors |
| 140 | + typed_config: |
| 141 | + "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors |
| 142 | + - name: envoy.filters.http.grpc_web |
| 143 | + typed_config: |
| 144 | + "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb |
81 | 145 | {{? it.platform.gateway.rateLimiter.enabled}} |
82 | 146 | - name: envoy.filters.http.ratelimit |
83 | 147 | typed_config: |
|
96 | 160 | timeout: 0.5s |
97 | 161 | transport_api_version: V3 |
98 | 162 | {{?}} |
99 | | - - name: envoy.filters.http.grpc_web |
100 | | - typed_config: |
101 | | - "@type": type.googleapis.com/envoy.extensions.filters.http.grpc_web.v3.GrpcWeb |
102 | | - - name: envoy.filters.http.cors |
103 | | - typed_config: |
104 | | - "@type": type.googleapis.com/envoy.extensions.filters.http.cors.v3.Cors |
105 | 163 | - name: envoy.filters.http.router |
106 | 164 | typed_config: |
107 | 165 | "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router |
|
198 | 256 | allow_methods: GET, PUT, DELETE, POST, OPTIONS |
199 | 257 | allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout |
200 | 258 | max_age: "1728000" |
201 | | - expose_headers: custom-header-1,grpc-status,grpc-message,code,drive-error-data-bin,dash-serialized-consensus-error-bin,stack-bin |
| 259 | + expose_headers: custom-header-1,grpc-status,grpc-message,code,drive-error-data-bin,dash-serialized-consensus-error-bin,stack-bin,ratelimit-reset,ratelimit-limit,ratelimit-remaining |
202 | 260 |
|
203 | 261 | static_resources: |
204 | 262 | listeners: |
|
0 commit comments