Skip to content

add support for logging streamed data in http_logger#497

Open
saninthottungal wants to merge 1 commit into
Frezyx:masterfrom
saninthottungal:master
Open

add support for logging streamed data in http_logger#497
saninthottungal wants to merge 1 commit into
Frezyx:masterfrom
saninthottungal:master

Conversation

@saninthottungal

@saninthottungal saninthottungal commented May 16, 2026

Copy link
Copy Markdown

First of all, this may not be the best way to implement this, i created a PR instead of creating this as an issue because you may get better context of what the feature is about. that being said, i am open to make any changes to this PR.

Thanks for the amazing SDK

Summary by Sourcery

Add support for logging HTTP streamed responses while preserving the original response stream behavior.

New Features:

  • Enable logging of StreamedResponse HTTP responses by reading and decoding the response stream for Talker logging.

Enhancements:

  • Ensure logged HTTP responses and returned responses are decoupled so logging does not consume or alter the original response stream.

Build:

  • Update example package dev dependency to flutter_lints ^6.0.0.

@sourcery-ai

sourcery-ai Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Adds support for logging streamed HTTP responses in TalkerHttpLogger by normalizing StreamedResponse into a regular Response for logging while returning an equivalent StreamedResponse to callers, and bumps the flutter_lints dev dependency in the example package.

Sequence diagram for handling streamed HTTP responses in TalkerHttpLogger

sequenceDiagram
  participant Client
  participant TalkerHttpLogger
  participant HttpServer as HttpServerOrBackend
  participant Talker

  Client->>TalkerHttpLogger: interceptResponse(response)
  TalkerHttpLogger->>HttpServer: response (may be StreamedResponse)
  Note over TalkerHttpLogger: Check if response is StreamedResponse
  alt response is StreamedResponse
    TalkerHttpLogger->>TalkerHttpLogger: response.stream.toBytes()
    TalkerHttpLogger->>TalkerHttpLogger: utf8.decode(bytes) -> body
    TalkerHttpLogger->>TalkerHttpLogger: create Response(resForTalker)
    TalkerHttpLogger->>TalkerHttpLogger: create StreamedResponse(resForReturn)
  else response is Response
    TalkerHttpLogger->>TalkerHttpLogger: resForTalker = response
    TalkerHttpLogger->>TalkerHttpLogger: resForReturn = response
  end

  alt statusCode < 400 and settings.enabled
    TalkerHttpLogger->>TalkerHttpLogger: settings.responseFilter(resForTalker)
    opt responseFilter allows
      TalkerHttpLogger->>Talker: logCustom(HttpResponseLog)
    end
  else settings.enabled
    TalkerHttpLogger->>TalkerHttpLogger: settings.errorFilter(resForTalker)
    opt errorFilter allows
      TalkerHttpLogger->>Talker: logCustom(HttpErrorLog)
    end
  end

  TalkerHttpLogger-->>Client: resForReturn
Loading

File-Level Changes

Change Details Files
Add support for logging StreamedResponse objects without consuming their stream for the caller.
  • Import dart:convert to decode response byte streams as UTF-8 strings.
  • Introduce separate response variables for logging and for returning to the caller, handling both StreamedResponse and non-streamed responses.
  • For StreamedResponse, buffer the byte stream, create a regular Response for logging, and recreate a StreamedResponse with the buffered bytes for the interceptor return value.
  • Update logging logic to use the normalized response object for status checks, filters, and log construction, while returning the potentially reconstructed response to the HTTP client.
packages/talker_http_logger/lib/talker_http_logger_interceptor.dart
Update example package linting dependency version.
  • Bump flutter_lints dev dependency from ^5.1.1 to ^6.0.0 in the example package pubspec.
packages/talker_http_logger/example/pubspec.yaml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 4 issues, and left some high level feedback:

  • Reading the entire StreamedResponse into memory with toBytes() before logging can be problematic for large responses; consider adding a size limit or truncation strategy to avoid excessive memory use while still capturing useful log information.
  • When decoding the streamed body with utf8.decode(bytes), you may want to respect the response’s Content-Type/charset (or at least handle non-UTF-8 data more defensively) to avoid decoding errors or corrupt logs for binary or differently encoded responses.
  • The StreamedResponseResponse conversion logic could be extracted into a small helper to reduce duplication, make the intent clearer, and centralize future adjustments (e.g., handling additional fields or edge cases).
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Reading the entire `StreamedResponse` into memory with `toBytes()` before logging can be problematic for large responses; consider adding a size limit or truncation strategy to avoid excessive memory use while still capturing useful log information.
- When decoding the streamed body with `utf8.decode(bytes)`, you may want to respect the response’s `Content-Type`/charset (or at least handle non-UTF-8 data more defensively) to avoid decoding errors or corrupt logs for binary or differently encoded responses.
- The `StreamedResponse``Response` conversion logic could be extracted into a small helper to reduce duplication, make the intent clearer, and centralize future adjustments (e.g., handling additional fields or edge cases).

## Individual Comments

### Comment 1
<location path="packages/talker_http_logger/lib/talker_http_logger_interceptor.dart" line_range="113" />
<code_context>
+
+    if (response is StreamedResponse) {
+      final bytes = await response.stream.toBytes();
+      final body = utf8.decode(bytes);
+      resForTalker = Response(
+        body,
</code_context>
<issue_to_address>
**issue:** Consider handling non-UTF8 / binary responses more defensively when decoding.

`utf8.decode(bytes)` will throw on non‑UTF8 bodies (e.g. binary or malformed payloads), causing the interceptor to fail instead of just logging. Consider using `utf8.decode(bytes, allowMalformed: true)` or wrapping the decode in try/catch and falling back to a safer representation (e.g. hex or `bytes.toString()`) when decoding fails.
</issue_to_address>

### Comment 2
<location path="packages/talker_http_logger/lib/talker_http_logger_interceptor.dart" line_range="112-121" />
<code_context>
+    BaseResponse resForReturn;
+
+    if (response is StreamedResponse) {
+      final bytes = await response.stream.toBytes();
+      final body = utf8.decode(bytes);
+      resForTalker = Response(
+        body,
+        response.statusCode,
+        headers: response.headers,
+        isRedirect: response.isRedirect,
+        persistentConnection: response.persistentConnection,
+        reasonPhrase: response.reasonPhrase,
+        request: response.request,
+      );
+
+      resForReturn = StreamedResponse(
+        Stream.value(bytes),
+        response.statusCode,
</code_context>
<issue_to_address>
**suggestion (performance):** Buffering the full streamed response body may be problematic for large payloads.

Reading the entire `StreamedResponse` into memory with `toBytes()` significantly increases memory usage and risks OOM for large or long-lived streams. Consider guarding this behavior (e.g., size limits, content-type checks, or a config flag to disable full-body logging for streamed responses) to avoid unbounded buffering.

Suggested implementation:

```
import 'dart:convert';

import 'package:talker/talker.dart';
import 'package:talker_http_logger/http_error_log.dart';

const int _kMaxLoggableStreamedResponseContentLength = 1024 * 1024; // 1 MiB

```

```
    if (response is StreamedResponse) {
      final contentLengthHeader = response.headers['content-length'];
      final contentLength = response.contentLength ??
          (contentLengthHeader != null ? int.tryParse(contentLengthHeader) : null);

      final shouldBufferBody =
          contentLength != null && contentLength <= _kMaxLoggableStreamedResponseContentLength;

      if (shouldBufferBody) {
        final bytes = await response.stream.toBytes();
        final body = utf8.decode(bytes);
        resForTalker = Response(
          body,
          response.statusCode,
          headers: response.headers,
          isRedirect: response.isRedirect,
          persistentConnection: response.persistentConnection,
          reasonPhrase: response.reasonPhrase,
          request: response.request,
        );

        resForReturn = StreamedResponse(
          Stream.value(bytes),
          response.statusCode,
          headers: response.headers,
          isRedirect: response.isRedirect,
          persistentConnection: response.persistentConnection,
          reasonPhrase: response.reasonPhrase,
          request: response.request,
        );
      } else {
        // Avoid buffering large or unknown-size streamed responses to prevent high memory usage.
        resForTalker = Response(
          '',
          response.statusCode,
          headers: response.headers,
          isRedirect: response.isRedirect,
          persistentConnection: response.persistentConnection,
          reasonPhrase: response.reasonPhrase,
          request: response.request,
        );

        // Do not consume the stream; pass the original response through.
        resForReturn = response;
      }

```

To make this behavior configurable (as hinted in the review), you may also want to:
1. Add a configuration option to the interceptor to control `_kMaxLoggableStreamedResponseContentLength` (or to completely disable streamed-body logging).
2. Use that configuration instead of the hard-coded `1 MiB` constant so library consumers can tune or disable buffering based on their needs.
</issue_to_address>

### Comment 3
<location path="packages/talker_http_logger/lib/talker_http_logger_interceptor.dart" line_range="114-123" />
<code_context>
+    if (response is StreamedResponse) {
+      final bytes = await response.stream.toBytes();
+      final body = utf8.decode(bytes);
+      resForTalker = Response(
+        body,
+        response.statusCode,
+        headers: response.headers,
+        isRedirect: response.isRedirect,
+        persistentConnection: response.persistentConnection,
+        reasonPhrase: response.reasonPhrase,
+        request: response.request,
+      );
+
+      resForReturn = StreamedResponse(
+        Stream.value(bytes),
+        response.statusCode,
</code_context>
<issue_to_address>
**question (bug_risk):** Re-wrapping `StreamedResponse` as `Response` for logging/filters changes the response type surface.

For streamed responses, `resForTalker` is now a `Response` instead of `StreamedResponse`, so any `responseFilter`/`errorFilter` relying on `StreamedResponse` (type checks or streaming-specific behavior) will no longer work as before. If this change isn’t intentional, consider preserving the original type for filters/logging (e.g., log metadata plus a separately captured body), or clearly document that streamed responses will appear as `Response` in the logger path.
</issue_to_address>

### Comment 4
<location path="packages/talker_http_logger/lib/talker_http_logger_interceptor.dart" line_range="130-133" />
<code_context>
+        persistentConnection: response.persistentConnection,
+        reasonPhrase: response.reasonPhrase,
+        request: response.request,
+        contentLength: bytes.length,
+      );
+    } else {
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Using `bytes.length` as `contentLength` may diverge from the original response semantics.

The original `StreamedResponse` may have an explicit `contentLength` (including `-1` when unknown). Overwriting it with `bytes.length` can change behavior for consumers that rely on `contentLength` (e.g., progress, buffering). Prefer preserving `response.contentLength` when it’s non-null and non-negative, and only fall back to `bytes.length` when the original length is unknown.

```suggestion
        reasonPhrase: response.reasonPhrase,
        request: response.request,
        contentLength: (response.contentLength != null &&
                response.contentLength! >= 0)
            ? response.contentLength
            : bytes.length,
      );
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.


if (response is StreamedResponse) {
final bytes = await response.stream.toBytes();
final body = utf8.decode(bytes);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue: Consider handling non-UTF8 / binary responses more defensively when decoding.

utf8.decode(bytes) will throw on non‑UTF8 bodies (e.g. binary or malformed payloads), causing the interceptor to fail instead of just logging. Consider using utf8.decode(bytes, allowMalformed: true) or wrapping the decode in try/catch and falling back to a safer representation (e.g. hex or bytes.toString()) when decoding fails.

Comment on lines +112 to +121
final bytes = await response.stream.toBytes();
final body = utf8.decode(bytes);
resForTalker = Response(
body,
response.statusCode,
headers: response.headers,
isRedirect: response.isRedirect,
persistentConnection: response.persistentConnection,
reasonPhrase: response.reasonPhrase,
request: response.request,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (performance): Buffering the full streamed response body may be problematic for large payloads.

Reading the entire StreamedResponse into memory with toBytes() significantly increases memory usage and risks OOM for large or long-lived streams. Consider guarding this behavior (e.g., size limits, content-type checks, or a config flag to disable full-body logging for streamed responses) to avoid unbounded buffering.

Suggested implementation:

import 'dart:convert';

import 'package:talker/talker.dart';
import 'package:talker_http_logger/http_error_log.dart';

const int _kMaxLoggableStreamedResponseContentLength = 1024 * 1024; // 1 MiB

    if (response is StreamedResponse) {
      final contentLengthHeader = response.headers['content-length'];
      final contentLength = response.contentLength ??
          (contentLengthHeader != null ? int.tryParse(contentLengthHeader) : null);

      final shouldBufferBody =
          contentLength != null && contentLength <= _kMaxLoggableStreamedResponseContentLength;

      if (shouldBufferBody) {
        final bytes = await response.stream.toBytes();
        final body = utf8.decode(bytes);
        resForTalker = Response(
          body,
          response.statusCode,
          headers: response.headers,
          isRedirect: response.isRedirect,
          persistentConnection: response.persistentConnection,
          reasonPhrase: response.reasonPhrase,
          request: response.request,
        );

        resForReturn = StreamedResponse(
          Stream.value(bytes),
          response.statusCode,
          headers: response.headers,
          isRedirect: response.isRedirect,
          persistentConnection: response.persistentConnection,
          reasonPhrase: response.reasonPhrase,
          request: response.request,
        );
      } else {
        // Avoid buffering large or unknown-size streamed responses to prevent high memory usage.
        resForTalker = Response(
          '',
          response.statusCode,
          headers: response.headers,
          isRedirect: response.isRedirect,
          persistentConnection: response.persistentConnection,
          reasonPhrase: response.reasonPhrase,
          request: response.request,
        );

        // Do not consume the stream; pass the original response through.
        resForReturn = response;
      }

To make this behavior configurable (as hinted in the review), you may also want to:

  1. Add a configuration option to the interceptor to control _kMaxLoggableStreamedResponseContentLength (or to completely disable streamed-body logging).
  2. Use that configuration instead of the hard-coded 1 MiB constant so library consumers can tune or disable buffering based on their needs.

Comment on lines +114 to +123
resForTalker = Response(
body,
response.statusCode,
headers: response.headers,
isRedirect: response.isRedirect,
persistentConnection: response.persistentConnection,
reasonPhrase: response.reasonPhrase,
request: response.request,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

question (bug_risk): Re-wrapping StreamedResponse as Response for logging/filters changes the response type surface.

For streamed responses, resForTalker is now a Response instead of StreamedResponse, so any responseFilter/errorFilter relying on StreamedResponse (type checks or streaming-specific behavior) will no longer work as before. If this change isn’t intentional, consider preserving the original type for filters/logging (e.g., log metadata plus a separately captured body), or clearly document that streamed responses will appear as Response in the logger path.

Comment on lines +130 to +133
reasonPhrase: response.reasonPhrase,
request: response.request,
contentLength: bytes.length,
);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): Using bytes.length as contentLength may diverge from the original response semantics.

The original StreamedResponse may have an explicit contentLength (including -1 when unknown). Overwriting it with bytes.length can change behavior for consumers that rely on contentLength (e.g., progress, buffering). Prefer preserving response.contentLength when it’s non-null and non-negative, and only fall back to bytes.length when the original length is unknown.

Suggested change
reasonPhrase: response.reasonPhrase,
request: response.request,
contentLength: bytes.length,
);
reasonPhrase: response.reasonPhrase,
request: response.request,
contentLength: (response.contentLength != null &&
response.contentLength! >= 0)
? response.contentLength
: bytes.length,
);

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 21.73913% with 18 lines in your changes missing coverage. Please review.
✅ Project coverage is 92.20%. Comparing base (1ed15ab) to head (cff8b69).
⚠️ Report is 223 commits behind head on master.

Files with missing lines Patch % Lines
...ttp_logger/lib/talker_http_logger_interceptor.dart 21.73% 18 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #497      +/-   ##
==========================================
- Coverage   98.63%   92.20%   -6.43%     
==========================================
  Files           3        7       +4     
  Lines         146      231      +85     
==========================================
+ Hits          144      213      +69     
- Misses          2       18      +16     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants