Interceptor Option Validation
Applies to
@connectum/interceptorsand@connectum/cli1.3.0.
Does this apply to you?
You need to act only if one of these is true:
- you pass a fractional number, or a delay above 2147483647 ms, to
createTimeoutInterceptor,createBulkheadInterceptor,createCircuitBreakerInterceptororcreateRetryInterceptor(directly or throughcreateDefaultInterceptors()); - you pass
fallback: truetocreateDefaultInterceptors(); - you pass
null, a number, a string, an array or a function as the value of a key ofcreateDefaultInterceptors()(for example{ timeout: null }or{ retry: 3 }), or anything other than a boolean asvalidation; - you pass a key to
createDefaultInterceptors()that it does not know (a typo such astimout, orlogger/methodFilter, which are not part of the default chain), ornullinstead of the options object; - you pass a value that is not a boolean to
skipStreaming,skipHealthCheck,includeTransport,includeBodies,skipGrpcServices,alwaysEmitImplicitorignoreUnknownFields, alevelof the logger other thandebug,info,warnorerror, aloggerorfailurePredicatethat is not a function,retryableCodesthat is not an array of Connect codes, ornullinstead of the options object of any factory; - you register a key in
createMethodFilterInterceptor()that is not*,Service/*orService/Method(for example/Unary,acme.v1.Svc/,*/*,acme.v1.Svc/Un*), a value that is not an array of functions, or anullentry in such an array; - your logs or alerts rely on the logger skipping a service only because its name contains
grpc.healthinside a longer segment (such asacme.grpc.healthcare.v1.Svc); - you run
connectum init --resiliencewithfallbackin the list (a script, a CI job or a template); - code or an alert matches the text of the bulkhead refusal.
If your options are already integers within the ranges below, your keys hold true, false or an options object, and the other options have the types above, nothing changes.
What changed
Until 1.3.0 these values were accepted and ran with different behavior than asked for:
| Value | Before | From 1.3.0 |
|---|---|---|
timeout.duration above 2147483647 | The platform timer fired after 1 ms: a very long timeout expired at once | Throws RangeError when the interceptor is created |
A fraction in capacity, queueSize, threshold, halfOpenAfter, initialDelay, maxDelay or duration | Passed on to the library as is | Throws RangeError |
fallback: true | Silently ignored: no fallback interceptor was added | Throws TypeError; the type is false | FallbackOptions |
null, 0, 5000, "30s", [] or a function as a key of createDefaultInterceptors() | The interceptor stayed off, with no message | Throws TypeError naming the key |
An unknown key of createDefaultInterceptors(), such as { timout: { duration: 10 } } or { logger: true } | Ignored: the interceptor you asked for was not added | Throws TypeError naming the key and listing the known keys |
null, a number, a string or an array in place of an options object (createTimeoutInterceptor(null), createDefaultInterceptors(null)) | A raw Cannot read properties of null, or silently read as empty | Throws TypeError naming the factory |
"no", "yes", 1, 0 or null for a boolean option such as skipStreaming or skipGrpcServices | Read as true or false by truthiness | Throws TypeError naming the option |
level: "bogus" for the logger | Accepted; nothing was logged | Throws RangeError listing debug, info, warn, error |
logger or failurePredicate that is not a function | Accepted; the first logged or failed call misbehaved | Throws TypeError |
retryableCodes: null, "14" or {} | Accepted; the first failing call became Internal, or nothing was retried | Throws TypeError; a number that is not a Connect code throws RangeError |
A method filter key such as /Unary, acme.v1.Svc/, */*, acme.v1.Svc/Un*, a/b/* or one with a space | Accepted; the interceptors under it never ran | Throws Error when the filter is created |
A method filter value that is a string, null, or an array with a null entry | A null entry was skipped; a string failed on the call | Throws TypeError naming the key |
Logger skipHealthCheck: true and a service named like acme.grpc.healthcare.v1.Svc | The service was never logged | Only the grpc.health package is skipped; the service is logged |
createFallbackInterceptor({}) | Error | TypeError |
connectum init --resilience fallback | Generated fallback: true, which did nothing | Refused with invalid --resilience "fallback" and a hint |
A value that is not a number throws TypeError; a number outside the rule throws RangeError. The message names the factory, the option, the rule and the received value. The accepted ranges are in Option ranges and validation. retry.maxRetries accepts the same values as before (a fraction such as 0.5 is still valid); only the error class for an invalid value changed.
The bulkhead refusal keeps the code ResourceExhausted. Its text is now Bulkhead capacity exceeded (capacity: N, queueSize: M) with the configured limits; before, it printed the policy's free slots under the label active, so a full bulkhead read as idle.
Required changes
- Round numbers and cap delays. Pass integers. For a very long timeout use at most 2147483647 ms (about 24.8 days); to run without a time limit, leave
timeoutout of the chain. - Replace
fallback: true. Passfallback: { handler }with a function that returns the response message, or remove the key. - Replace non-object values. Use
falseto disable an interceptor,truefor its defaults, or an options object. Do not passnull,0or a number to mean "off" or "use this value". - Fix or remove unknown keys. Spell the key as listed in Customizing the Default Chain, and add the logger or the method filter to the
interceptorsarray with their own factories. - Pass real booleans, functions, levels and codes. Use
trueorfalsefor flags, a function forloggerandfailurePredicate, one ofdebug,info,warn,errorforlevel, and an array ofCodevalues forretryableCodes. - Correct method filter keys. Use
*,Service/*orService/Methodwith the exact protobuf names, and put every interceptor in an array. A key that used to be accepted never matched, so its interceptors were not running; fixing the key turns them on, which can change the behavior of the services it now covers. - Check what the logger skips. If you relied on the logger omitting a service whose name contains
grpc.healthinside a longer segment, set up your own filter, for example withcreateMethodFilterInterceptor. - Drop
fallbackfrom--resilience. The remaining values aretimeout,bulkhead,circuitBreakerandretry. Add the fallback interceptor tosrc/server.tsyourself. - Match the code, not the text. If anything matches the bulkhead message, compare
err.codewithCode.ResourceExhausted.
Before and after
import { createDefaultInterceptors } from '@connectum/interceptors';
// Before: accepted
createDefaultInterceptors({
timeout: { duration: 2_592_000_000 }, // 30 days: the timer fired after 1 ms
bulkhead: { capacity: 2.5 },
retry: null, // looked configured, retry stayed off
fallback: true, // looked enabled, did nothing
});
// From 1.3.0
createDefaultInterceptors({
timeout: { duration: 2_147_483_647 }, // the longest delay the timer honors
bulkhead: { capacity: 3 },
retry: false, // or true, or an options object
fallback: { handler: () => ({ items: [] }) },
});2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Fallback: validation order and handler result
You need to act if you enable fallback in createDefaultInterceptors() or build a chain with createFallbackInterceptor.
| Case | Before | Now |
|---|---|---|
fallback enabled, a request breaks a validation rule | The caller got the fallback value as a successful response | The caller gets INVALID_ARGUMENT; neither the handler nor the fallback handler runs |
The fallback handler returns a plain object with the response fields ({ items: [] }) | On a server the call failed with a serialize binary: ... text | The object is turned into the response message |
The handler returns undefined, null, a string, or an object that does not fit the response type (an unknown key, a number in a string field, a non-array in a repeated field, ...) | undefined failed on a server with a serialize binary: ... text; the other values were not checked and were passed on as the response | TypeError naming createFallbackInterceptor, the expected type and the path of the problem; behind the error handler the client gets Internal: internal error |
With fallback enabled, the default chain is errorHandler, timeout, bulkhead, validation, fallback, circuitBreaker, retry, serializer. Without fallback, validation stays after retry. See Fallback position for the retry and breaker behavior.
Action: if you compose the chain yourself, put the validation interceptor before the fallback interceptor. If your handler returns a plain object, check that every key is a field of the method's response; if it returns something else, return create(OutputSchema, { ... }). If something relied on a rejected request being answered by the fallback value, handle INVALID_ARGUMENT in the caller instead.
Verify
Start the service: every refused value throws while the chain is built, before the first request, so a configuration that starts is valid. To check a configuration in a test, call the factory or createDefaultInterceptors() with the same options and assert that it does not throw.