Java
Spring Boot
StreamingResponseBody
Exception Handling
Error Management

Exception handling in StreamingResponseBody

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

StreamingResponseBody is useful when a controller needs to send large files or generated output without buffering the full response in memory. The hard part is error handling, because once bytes start going to the client, Spring often cannot replace the response with a normal JSON error body.

How Streaming Changes Error Handling

A normal Spring MVC controller builds a response and returns it before anything is written to the socket. With StreamingResponseBody, your controller returns quickly and the actual write happens later on an output stream. That changes the failure model.

If an exception happens before the response is committed, Spring can still set a status code and hand control to normal exception resolvers. If an exception happens after streaming begins, the client may receive a partial payload and then a broken connection.

A typical controller looks like this:

java
1import java.io.BufferedWriter;
2import java.io.OutputStreamWriter;
3import java.nio.charset.StandardCharsets;
4import org.springframework.http.HttpHeaders;
5import org.springframework.http.MediaType;
6import org.springframework.http.ResponseEntity;
7import org.springframework.web.bind.annotation.GetMapping;
8import org.springframework.web.bind.annotation.RestController;
9import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody;
10
11@RestController
12public class ExportController {
13
14    @GetMapping("/export")
15    public ResponseEntity<StreamingResponseBody> export() {
16        StreamingResponseBody body = outputStream -> {
17            try (BufferedWriter writer = new BufferedWriter(
18                    new OutputStreamWriter(outputStream, StandardCharsets.UTF_8))) {
19                writer.write("id,name\n");
20                writer.write("1,Ada\n");
21                writer.write("2,Grace\n");
22                writer.flush();
23            }
24        };
25
26        return ResponseEntity.ok()
27                .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=users.csv")
28                .contentType(MediaType.TEXT_PLAIN)
29                .body(body);
30    }
31}

That is the easy case. Real applications stream from a database, remote service, or generated archive, and any of those layers can fail mid-stream.

Where to Catch Exceptions

The safest rule is to fail as early as possible. If you can validate input, open resources, or confirm authorization before returning the StreamingResponseBody, do it there. Those failures still behave like normal controller exceptions.

Inside the streaming callback, catch exceptions that can happen while writing. At that point you usually cannot send a structured error response, so your goals are narrower:

  • stop writing cleanly
  • log enough detail for diagnosis
  • release resources
  • avoid masking the original exception
java
1@GetMapping("/numbers")
2public ResponseEntity<StreamingResponseBody> numbers() {
3    StreamingResponseBody body = outputStream -> {
4        try (BufferedWriter writer = new BufferedWriter(
5                new OutputStreamWriter(outputStream, StandardCharsets.UTF_8))) {
6            for (int i = 1; i <= 5; i++) {
7                writer.write("value=" + i + "\n");
8                writer.flush();
9            }
10        } catch (Exception ex) {
11            // Replace with your logger in production
12            System.err.println("Streaming failed: " + ex.getMessage());
13            throw ex;
14        }
15    };
16
17    return ResponseEntity.ok().body(body);
18}

Re-throwing is useful because the container can still record the failure, but you should not expect @ControllerAdvice to build a neat fallback body once output has been committed.

Practical Patterns

For downloadable files, decide whether a partial response is acceptable. CSV exports often are not, so build enough state in advance to reduce the chance of failure after commit. For event streams or long-running progress feeds, partial output may be normal, and the client should already be prepared for reconnects.

When a service layer can fail, isolate that failure before streaming starts:

java
1@GetMapping("/report")
2public ResponseEntity<StreamingResponseBody> report() {
3    ReportGenerator generator = loadGeneratorOrThrow();
4
5    StreamingResponseBody body = outputStream -> generator.writeCsv(outputStream);
6
7    return ResponseEntity.ok()
8            .contentType(MediaType.TEXT_PLAIN)
9            .body(body);
10}

That pattern keeps expensive setup and permission checks out of the streaming lambda.

What @ControllerAdvice Can and Cannot Do

Global exception handlers still matter, but mainly for failures that happen before bytes are sent. They are useful for validating parameters, rejecting missing resources, and handling setup errors.

For mid-stream failures, logging, metrics, and client expectations matter more than error-page formatting. If clients need structured status updates, a stream protocol such as server-sent events with explicit message boundaries is often easier to recover from than a binary download.

Common Pitfalls

The biggest mistake is expecting a mid-stream exception to produce the same error JSON as a normal controller method. Once the response is committed, that is usually no longer possible.

Another common issue is forgetting to flush output periodically for long responses. Without flushing, the client may wait too long and appear stuck.

Some implementations also catch Exception and swallow it. That hides production failures and makes broken downloads look random.

Finally, avoid opening files, database cursors, or remote streams without closing them inside the streaming callback. try with resources is still the right tool here.

Summary

  • 'StreamingResponseBody changes when and how exceptions surface.'
  • Fail before streaming starts whenever possible.
  • Catch, log, and clean up exceptions inside the streaming callback.
  • Do not rely on @ControllerAdvice after the response is committed.
  • Design clients to tolerate partial streams when the protocol allows it.

Course illustration
Course illustration

All Rights Reserved.