Skip to content

feat(http): implement body inspection handler - #736

Open
RKS (rksharma-owg) wants to merge 13 commits into
microsoft:mainfrom
rksharma-owg:feat/body-inspection-handler
Open

RKS (rksharma-owg) wants to merge 13 commits into
microsoft:mainfrom
rksharma-owg:feat/body-inspection-handler

Conversation

@rksharma-owg

@rksharma-owg RKS (rksharma-owg) commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Resolves #418

Description

Implements the BodyInspectionHandler middleware and BodyInspectionHandlerOption for Kiota Python, following the shared middleware specification in microsoftgraph/msgraph-sdk-design#116.

Summary of Changes

  • Adds opt-in request and response body capture with byte and rewound BytesIO accessors.
  • Registers BodyInspectionHandler in the default middleware pipeline.
  • Preserves request and response payloads for downstream consumers, including replayable raw streamed responses.
  • Carries request options through redirects using native HTTPX request extensions, while retaining the existing middleware compatibility bridge.
  • Clears captured state before each use and isolates captures by execution context when concurrent requests share a handler-level option.
  • Restores HTTPX download-byte accounting, decoded-content state, and decoder lifecycle so replayed compressed streams are consumed and counted normally.
  • Leaves already-consumed responses without cached content and closed unreadable responses unchanged instead of raising during inspection.
  • Avoids initializing or copying capture state on the default-disabled path while still clearing reused captures.
  • Retains the original lifecycle state for already-buffered responses instead of exposing decoded bytes as a raw stream.
  • Rejects a null request with an intentional TypeError before option or span access.
  • Emits the standardized com.microsoft.kiota.handler.bodyInspection.enable telemetry attribute and keeps the span open through inspection, transport execution, response handling, and exception cleanup.
  • Adds regression coverage for capture behavior, concurrent requests, byte accounting, request and response streaming, redirect propagation, state reset, null-request validation, span lifecycle, telemetry, and client factory integration.

Documentation

Verification

  • pytest in packages/http/httpx: 154 passed.
  • Focused body-inspection tests: 25 passed.
  • Redirect, adapter, and body-inspection tests: 102 passed.
  • YAPF and targeted isort checks passed for changed files.
  • Pylint rated the package 10.00/10; the repository emits its existing suggestion-mode configuration warning.
  • Mypy reported no issues in 26 source files.
  • Fork preflight passed the full Python 3.10-3.14 matrix for commit eaa50db: https://github.com/rksharma-owg/kiota-python/actions/runs/35131242553
  • SonarCloud reports 0 open or confirmed new issues for commit eaa50db.

- Add BodyInspectionHandlerOption with inspect_request_body and inspect_response_body flags
- Add BodyInspectionHandler middleware to non-destructively inspect request and response bodies
- Provide byte accessors and rewound BytesIO stream helpers
- Register BodyInspectionHandler in KiotaClientFactory default middleware pipeline
- Add comprehensive unit tests covering options, request/response capture, streaming, and pipeline integration
@github-actions

This comment was marked as outdated.

This comment was marked as outdated.

@baywet

Copy link
Copy Markdown
Member

RKS (@rksharma-owg) would you mind addressing the copilot comments please?

@rksharma-owg

Copy link
Copy Markdown
Contributor Author

Addressed the review findings in ee0ea54: standardized the telemetry key, preserved per-request options across redirects, restored replayable raw response streams after inspection, and added regressions for each behavior. The full Python 3.10-3.14 fork preflight passed, and the linked documentation update is MicrosoftDocs/openapi-docs#175.

This comment was marked as outdated.

Comment thread packages/http/httpx/kiota_http/middleware/body_inspection_handler.py Outdated
@rksharma-owg

Copy link
Copy Markdown
Contributor Author

Addressed the new review in 1dc4c07. Reused per-request options now clear stale captures, and already-buffered responses retain their original consumed/closed raw-stream state. The request-stream concern was verified against HTTPX 0.25.0-0.28.1 and is already handled by Request.aread(); the regression now exercises request.stream directly at the transport boundary. Local validation reports 133 tests passing, and the full Python 3.10-3.14 fork preflight passed: https://github.com/rksharma-owg/kiota-python/actions/runs/35100712297

This comment was marked as outdated.

@rksharma-owg

Copy link
Copy Markdown
Contributor Author

Addressed both findings from the latest Copilot review in b80f4fc:

  • handler-level capture state is now isolated per execution context, with a concurrent forced-overlap regression test;
  • response restoration now also restores HTTPX byte accounting, with coverage confirming the count stays at its pre-inspection value until the caller consumes the replayed stream.

Local validation is green (136 HTTP package tests; 21 focused tests), and the full Python 3.10–3.14 fork preflight passed: https://github.com/rksharma-owg/kiota-python/actions/runs/35104897911

This comment was marked as outdated.

@rksharma-owg

Copy link
Copy Markdown
Contributor Author

I also addressed the decoded-stream lifecycle finding in ae25c49. Rewinding now clears the temporary decoded-content cache and HTTPX's stateful decoder before restoring the raw stream, so a caller using aiter_bytes() consumes the replayed stream normally, closes the response, and updates num_bytes_downloaded correctly. The regression uses a gzip stream to verify decoded content, lifecycle flags, and raw-byte accounting.\n\nValidation is green: 138 HTTP package tests, 23 focused body-inspection tests, HTTPX 0.25.0–0.28.1 compatibility checks, and the full Python 3.10–3.14 fork preflight: https://github.com/rksharma-owg/kiota-python/actions/runs/35109454408

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Address the two moderate issues involving unnecessary per-request allocations and closed-response handling.

Review details

Suppressed comments (2)

packages/http/httpx/kiota_http/middleware/body_inspection_handler.py:103

  • Because this handler is now present in every default pipeline, these unconditional resets allocate a new WeakKeyDictionary twice for every request even when both inspection flags are disabled. That adds avoidable per-request overhead to the default client; initialize/update the context capture map only when inspection is enabled or there is existing state that must be cleared, while retaining the reset behavior for reused options.
        # Clear body per request
        current_options.request_body = None
        current_options.response_body = None

packages/http/httpx/kiota_http/middleware/body_inspection_handler.py:69

  • A response can be closed without being marked stream-consumed (Response.aclose() sets is_closed), so this condition still calls aiter_raw() on an unreadable stream and raises httpx.StreamClosed. Treat closed responses like other unrecoverable streams and skip inspection instead of turning a valid downstream response into an exception.
                elif not response.is_stream_consumed:
  • Files reviewed: 8/8 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@rksharma-owg

Copy link
Copy Markdown
Contributor Author

Thanks for the extra pass — both moderate findings are addressed in 22d08e5:\n\n- capture state is now cleared only when a prior request/response body actually exists, so the default-disabled handler leaves the context map uninitialized while reused options still reset correctly;\n- response inspection now skips closed, uncached streams just as it skips consumed, uncached streams, preserving the original response instead of raising StreamClosed.\n\nI added focused regressions for both paths. Validation is green: 140 HTTP package tests, 25 focused body-inspection tests, and the full Python 3.10–3.14 fork preflight: https://github.com/rksharma-owg/kiota-python/actions/runs/35113744365

Comment thread packages/http/httpx/kiota_http/middleware/redirect_handler.py Outdated
Comment thread packages/http/httpx/kiota_http/middleware/body_inspection_handler.py Outdated
Comment thread packages/http/httpx/kiota_http/middleware/redirect_handler.py

This comment was marked as outdated.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Moderate telemetry and span-parent propagation issues remain unresolved.

Review details

Suppressed comments (3)

Previously missed (1) — in code that hasn't changed since the last review.

packages/http/httpx/kiota_http/middleware/body_inspection_handler.py:53

  • This attribute is emitted as true even when both inspection flags are at their documented default of False, so spans falsely report body inspection as enabled on every default-pipeline request. The specification defines this enable attribute as true only when the handler is enabled; set it from the request/response inspection flags (and add a disabled-path assertion).

packages/http/httpx/kiota_http/middleware/body_inspection_handler.py:99

  • This new native-extension path can select the body option, but _create_observability_span still reads only the legacy request.options attribute (BaseMiddleware._create_observability_span, lines 73-80). For extension-only requests such as the added redirect regression, the body-inspection span therefore loses the parent_span carried in REQUEST_OPTIONS_KEY and is not nested under the request span; make the span helper consume the same extension bridge (while retaining the legacy fallback).
        request_options = request.extensions.get(REQUEST_OPTIONS_KEY)
        if request_options is None:
            request_options = getattr(request, "options", None)
        if request_options:
            current_options = request_options.get(BodyInspectionHandlerOption.get_key(), None)

packages/http/httpx/kiota_http/middleware/redirect_handler.py:69

  • The handler now accepts REQUEST_OPTIONS_KEY request options, but both spans in send are created through _create_observability_span, which only reads the legacy request.options attribute. An extension-only request consequently loses its carried parent_span for redirect telemetry; update the shared span lookup to read the native extension first and retain the legacy fallback.
        request_options = request.extensions.get(REQUEST_OPTIONS_KEY)
        if request_options is None:
            request_options = getattr(request, "options", None)
  • Files reviewed: 11/11 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

current_options = self._get_current_options(request)
request_options = request.extensions.get(REQUEST_OPTIONS_KEY)
if request_options is None:
request_options = getattr(request, "options", None)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we're still relying on reflection here, and a number of other places. Can you please clean it up?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cleaned up reflection across all middleware handlers (redirect_handler, body_inspection_handler, headers_inspection_handler, parameters_name_decoding_handler, retry_handler, url_replace_handler, user_agent_handler, and BaseMiddleware). All handlers now read options directly from HTTPX's native extension dictionary via request.extensions.get(REQUEST_OPTIONS_KEY), and monkey-patched request.options has been eliminated. All unit and middleware tests pass.

@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

http - implement body inspection handler

3 participants