fix: missing exports, context skip_methods, and compression "none" - #17
Merged
Conversation
added 4 commits
September 6, 2026 21:11
The base class has taken `skip_methods` since it was written, and the guide documents it as "a constructor kwarg" without qualification, but `AsyncContextInterceptor.__init__` neither accepted it nor forwarded one -- `AsyncContextInterceptor([...], skip_methods=SKIPPED_HEALTH_METHODS)` raised `TypeError`. Health traffic therefore always ran through header extraction, and with a `required=True` HeaderConfig a kubelet probe (which carries none of the service's headers) was aborted with INVALID_ARGUMENT. The kwarg is added keyword-only and defaults to `()`, so existing behaviour is unchanged: unlike the observability interceptors, this one binds values the handler itself may read, so skipping health methods is an opt-in rather than a new default.
`COMPRESSION_ALGORITHMS` maps "none" to gRPC's GRPC_COMPRESS_NONE, and `build_grpc_options` has always translated it (there is a test for the mapping), so `GrpcServerConfig(compression_algorithm="none")` builds a server fine. `BaseGrpcServerSettings` typed the same field as `Literal["deflate", "gzip"] | None` and rejected it with a ValidationError -- the two settings shapes are meant to be interchangeable everywhere the kit accepts settings. Widening the literal rather than dropping "none" from the option builder: "none" is an explicit request for gRPC's no-compression algorithm, which is not the same statement as leaving the field unset.
Two names a caller has to reach into a submodule for while everything next to them is available from the package root: - `GrpcServiceName` is the DI key the kit's own dishka providers register the service name under, so anyone writing their own provider needs it, yet the root re-exported the five protocols beside it and not this one. It is now exported from `grpc_server_kit` and `grpc_server_kit.aio`. - `grpc_server_kit.aio` exported `reset_signal_handlers` but not `reset_signal_handlers_async`, the form `ServerLifecycleManager` uses internally and the docstrings recommend, nor `SignalManager` -- which `ServerLifecycleManager(signal_manager=...)` and `run_async_grpc_server(signal_manager=...)` both accept. Additions only; nothing is renamed or removed. `implicit_reexport` is off for mypy, so a name missing from `__all__` was not importable for type checkers either -- the tests assert both the attribute and `__all__`.
"When health is enabled, the health service name is advertised via reflection automatically" is only half the story: `GrpcApp.build()` appends `grpc.health.v1.Health` to the reflection list when `enable_reflection([...])` already gave it one, and if reflection was turned on solely through `settings.enable_reflection = True` the build raises ValueError instead. The code is right -- reflection with no service names is a misconfiguration worth failing on, and the agents page already states the rule -- so the quickstart line is the one that drifted.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Five defects that came out of writing
docs/agents.mdagainst the source. All fivereproduce; each fix is additive, nothing is renamed, removed or re-defaulted.
1.
AsyncContextInterceptorcould not be givenskip_methodsAsyncServerInterceptorhas takenskip_methodssince it was written, anddocs/guide/interceptors.mddocuments it as "a constructor kwarg" withoutqualification, but
AsyncContextInterceptor.__init__neither accepted one norforwarded one — it called
super().__init__()bare.So every health probe went through header extraction, and with a
required=TrueHeaderConfiga kubelet probe — which carries none of the service's headers — wasaborted with
INVALID_ARGUMENT.Fixed: the kwarg is keyword-only and defaults to
(), so behaviour is unchanged foreveryone. I deliberately did not default it to
SKIPPED_HEALTH_METHODSlike thefour observability interceptors do: this one binds values the handler itself may read,
so skipping is an opt-in, not a new default. Two tests in
tests/unit/aio/interceptors/test_context.py(bothTypeErrorbefore the change).One correction to the finding as written: this is not "the one interceptor with no way
to opt a method out".
AsyncExceptionHandlerInterceptorhas noskip_methodseither.I left that one alone — an interceptor whose whole job is mapping exceptions to statuses
has no business skipping RPCs — and made the agents-page table say so instead of leaving
the gap unexplained.
2.
BaseGrpcServerSettingsrejectedcompression_algorithm="none"COMPRESSION_ALGORITHMSmaps"none"to gRPC'sGRPC_COMPRESS_NONE, andbuild_grpc_optionshas always translated it (tests/unit/test_options.pyasserts the"none" -> 0mapping), soGrpcServerConfig(compression_algorithm="none")builds aserver fine. The pydantic model typed the same field
Literal["deflate", "gzip"] | None:The two settings shapes are documented as interchangeable everywhere the kit accepts
settings, so this was a real hole. Fixed by widening
GrpcCompressionAlgorithmtoLiteral["none", "deflate", "gzip"]rather than dropping"none"from the optionbuilder:
"none"is an explicit request for gRPC's no-compression algorithm, which isnot the same statement as leaving the field
None. Parametrized test intests/unit/test_settings.py(fails on[none]before the change).3.
GrpcServiceNamewas not re-exported at the package rootgrpc_server_kit.protocolsexports it, the kit's own dishka providers register theservice name under it, and the root re-exported the five protocols beside it and not
this one — so anyone writing their own provider had to import from the submodule:
Fixed: exported from
grpc_server_kitand fromgrpc_server_kit.aio(the agents pagedescribes
aioas "everything above plus …", which only stays true if both get it).4.
grpc_server_kit.aioexportedreset_signal_handlersbut not the async formaio.reset_signal_handlers_asyncandaio.SignalManagerwere both missing, thoughServerLifecycleManagerusesreset_asyncinternally, the docstrings recommend theasync form, and
ServerLifecycleManager(signal_manager=…)/run_async_grpc_server(signal_manager=…)both take aSignalManager.Fixed: both re-exported.
implicit_reexportis off for mypy, so a name missing from__all__was not importable for type checkers either — the tests intests/unit/test_init.pyassert the attribute and__all__membership.5. The quickstart was wrong about health and reflection
Half the story.
GrpcApp.build()appendsgrpc.health.v1.Healthonly whenenable_reflection([...])already gave it a list; reflection turned on solely throughsettings.enable_reflection = Truefails the build:Here the doc is what drifted, not the code: reflection with no service names is a
misconfiguration worth failing the build on, and
docs/agents.mdalready states therule correctly. Corrected the quickstart line. Documentation only.
Agents page
docs/agents.mdchanged with the API, perCONTRIBUTING.md:AsyncContextInterceptoris "the only one withno
skip_methodsargument" — it has one, and row 5 now records that the exceptionhandler is the one without;
COMPRESSION_ALGORITHMSvsBaseGrpcServerSettingsis gone — there is no asymmetry left;GrpcServiceNamemoved from "not re-exported at the package root" into the root APItable, and the
aioparagraph and lifecycle table list the signal names it nowre-exports.
docs/guide/interceptors.mdgains a short paragraph on the context interceptor's emptyskip_methodsdefault and why you would passSKIPPED_HEALTH_METHODSto it.Verification
uv sync --frozen --all-extras --group dev;uv.lockunchanged.No changelog entry:
CONTRIBUTING.mdleavesCHANGELOG.mdto Release Please.Nothing here is breaking
Three added names, one added keyword-only parameter with a behaviour-preserving default,
one widened
Literal, and two doc corrections.