skip to content

In REST Assured, you set LogConfig.defaultStream() but log() still prints to System.out. Why?

level: seniorimportance: nice to knowfreq 31%

answer

  1. the destination is configurable
  2. LogConfig owns the stream
  3. defaultStream takes a PrintStream
  4. the stream is read when log() runs
  5. config before log, never after

basics

~20 s

REST Assured resolves the print stream when log() is called, not when the request is sent. A config() applied after log() in the chain arrives too late, so the already-registered logging filter still holds System.out. Set the config first.

solid answer

~40 s

`LogConfig.defaultStream(PrintStream)` is what moves `log()` output off `System.out`; a default `LogConfig` is constructed with `System.out`, which is why an unconfigured suite prints to the console. Apply it globally with `RestAssured.config = RestAssuredConfig.config().logConfig(LogConfig.logConfig().defaultStream(ps))`, on a `RequestSpecBuilder.setConfig(...)`, or per call with `given().config(...)`. The trap is ordering. REST Assured reads the stream out of the request specification's `LogConfig` at the moment you call `given().log().all()` and bakes it into the `RequestLoggingFilter` it registers, while `given().config(cfg)` is a plain setter that mutates the spec afterwards. So `given().log().all().config(cfg)` prints to `System.out` and `given().config(cfg).log().all()` prints to your stream. The response side reads the same `LogConfig` off the request specification at `then().log()` time, so it follows the same rule. A global `RestAssured.config` assignment sidesteps the trap entirely, because `given()` copies it before any `log()` call runs.

code

java · 22 lines
java
import io.restassured.config.LogConfig;
import io.restassured.config.RestAssuredConfig;
import java.io.ByteArrayOutputStream;
import java.io.PrintStream;

import static io.restassured.RestAssured.given;

ByteArrayOutputStream captured = new ByteArrayOutputStream();
PrintStream ps = new PrintStream(captured, true);
RestAssuredConfig cfg = RestAssuredConfig.config()
        .logConfig(LogConfig.logConfig().defaultStream(ps));

given()
    .config(cfg)            // config BEFORE log(), or the stream is ignored
    .log().uri()
.when()
    .get("/seed-bank/accessions/SB-2291")
.then()
    .log().status()
    .statusCode(200);

String evidence = captured.toString();

go deeper

for a junior

Know that log() writes to System.out by default and that LogConfig.defaultStream takes a PrintStream. You are not expected to have wired a custom destination yourself at this stage.

for a middle

Explain that the stream is read out of the request specification's LogConfig at the moment log() is called and stored in the filter, which is why the order of config() and log() in a chain decides where output lands.

for a senior

Diagnose the symptom end to end: output still on the console despite a configured stream. Check the discarded immutable copy, then the chain order, then any specification built before the config existed.

for a principal

Decide where diagnostics belong for the whole suite — console, per-case buffer, or an attached artifact — and set that once, so no test author has to reason about streams or chain order again.

## The default is `System.out`, and it lives in `LogConfig` Every `log()` call in REST Assured writes to a `java.io.PrintStream`, and that stream is not hard-coded at the call site. It is held by `LogConfig`, one of the eighteen configuration objects inside `RestAssuredConfig`. A default-constructed `LogConfig` is built with `System.out`, which is why an unconfigured suite prints to the console. The class exposes both halves of the setting under one name: - `LogConfig.defaultStream()` — no argument, reads the current `PrintStream` back. - `LogConfig.defaultStream(PrintStream)` — returns a **new** `LogConfig` carrying the stream you passed. - `LogConfig.logConfig()` — the static factory that gives you a fresh instance to start from. That second point matters: the config objects are immutable and every setter returns a copy, so `logConfig().defaultStream(ps)` is only useful if you keep the returned object and install it somewhere. The same class also carries `enablePrettyPrinting(boolean)`, which decides whether a JSON or XML body is reformatted before printing, and `urlEncodeRequestUri(boolean)`, which decides whether the `Request URI:` line shows the encoded or the decoded URL — so the object you install governs the shape of the output as well as its destination. ## Three places to install it 1. Globally, on the static: `RestAssured.config = RestAssuredConfig.config().logConfig(LogConfig.logConfig().defaultStream(ps));` 2. On a shared builder: `new RequestSpecBuilder().setConfig(cfg)`. 3. Per call, on the request: `given().config(cfg)`. All three end in the same place — the request specification's `RestAssuredConfig` — and that is true of the response side as well, because `then().log()` reaches back to the request specification to find its `LogConfig`. There is no separate response-side stream setting. ## Why a correctly-built config can look ignored The stream is resolved **eagerly, at the `log()` call**, and stored inside the logging filter that call registers. Nothing re-reads the config later. So a chain that configures logging after asking for it configures nothing: | Chain | Where the output lands | |---|---| | `given().config(cfg).log().all()` | your stream | | `given().log().all().config(cfg)` | `System.out` | | `RestAssured.config = cfg;` then `given().log().all()` | your stream | | `new RequestSpecBuilder().setConfig(cfg).build()` used with `given().spec(...)` | your stream | Mechanically the sequence is: `given().log()` hands you a log spec; the narrowing call on it looks up the request specification's `LogConfig`, takes `defaultStream()`, and constructs a logging filter around that exact `PrintStream`; the filter is appended to the specification's filter list; `config(cfg)` later replaces a field the filter no longer consults. The same eager read applies to the response side at `then().log()`. ## How to diagnose it in a real suite When a seed-bank suite is meant to attach its request and response blocks to a per-case report and the console gets them instead, work through this in order: 1. Confirm you kept the object returned by `defaultStream(...)` — an immutable setter whose result is discarded is the most common cause of all. 2. Look at where `config(...)` sits relative to `log()` in every chain, including chains assembled across helper methods. 3. Check whether a specification built earlier snapshotted an older config, and rebuild it after the config is set. 4. Prefer one global assignment or one shared builder over per-call `config(...)`, so ordering cannot bite an individual test author. 5. Remember there is no per-call stream argument: `all()` takes a `boolean` for pretty-printing, not a `PrintStream`. 6. Print something unconditionally while you debug the plumbing — a narrow `given().log().uri()` is enough to prove where output is going without flooding the run. ## Choosing a destination Once the plumbing is right, the interesting decision is what the stream should be: - `System.out` is fine for a small suite run by a human watching the terminal. - A `PrintStream` wrapping a `ByteArrayOutputStream` lets you read the block back as a string and attach it to a case's evidence rather than scrolling a CI log. - A file-backed stream keeps large seed-bank inventory payloads out of the pipeline transcript while still preserving them. One caveat is worth stating out loud: a single stream installed on the `RestAssured` static is shared by every request the JVM makes, so concurrent cases interleave their blocks in it. If per-case evidence is the goal, each case needs its own `PrintStream` and its own config, which is an argument for building the config where the case is built rather than once in a static initialiser. The `LogConfig` object is cheap to construct, so there is no cost to doing so beyond deciding where in the suite the decision belongs. ## What the setting does not do Two boundaries are worth stating, because both come up: - It does not turn logging on. Nothing is printed until some `log()` call asks for it; `defaultStream` only decides where that print lands. - It does not give the request and the response separate destinations. Both sides read the same `LogConfig` from the same request specification, so one stream receives both blocks in the order they are produced. If you genuinely need the two blocks apart, the split has to happen after the fact, by parsing the captured text or by using two different specifications — not by configuration.

  • Where can you install a LogConfig so that chain order stops mattering?
    Assign it to the `RestAssured.config` static, or set it on a `RequestSpecBuilder` with `setConfig(...)`. `given()` copies the static config into the new request specification before any `log()` call runs, so the stream is already in place by the time a logging filter is constructed.
  • What type does LogConfig.defaultStream take, and how do you capture output in memory?
    A `java.io.PrintStream`. Wrap a `ByteArrayOutputStream` in one, hand that to `defaultStream(...)`, and read the bytes back after the call. Give each case its own stream if you want per-case evidence — one shared buffer collects every concurrent request's block into the same text.

saying these in an interview costs you the question

  • Thinks log() output can only go to System.out
  • Expects config() to apply retroactively to an earlier log() call
  • Discards the LogConfig returned by defaultStream and reuses the old one
  • Assumes the stream is resolved when the request is sent
  • Looks for a PrintStream argument on log().all()