• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

zalando / logbook / 5763

27 Jul 2026 11:09AM UTC coverage: 96.601% (-0.3%) from 96.858%
5763

push

github

web-flow
Fix JSON formatter embedding non-JSON body as raw value (#2326)

* Fix JSON formatter embedding non-JSON body as raw value

When a request body has a JSON content-type header but contains
non-JSON content (e.g. Base64-encoded ciphertext, binary data, or
truncated/malformed JSON), the formatters were embedding the raw
body directly using @JsonRawValue / writeRawValue(), producing
malformed JSON log output that breaks Logstash and Elasticsearch
ingestion.

Fix applied to all four formatters:
- JsonHttpLogFormatter (Jackson 3)
- FastJsonHttpLogFormatter (Jackson 3)
- JsonHttpLogFormatterJackson2 (Jackson 2)
- FastJsonHttpLogFormatterJackson2 (Jackson 2)

Each formatter now uses a two-stage validation approach:
1. Fast path: check first non-whitespace character against valid
   JSON start characters ({ [ " digit - t f n). Obvious non-JSON
   content like ciphertext is rejected immediately at zero parse cost.
2. Slow path: only if the fast check passes, attempt full JSON
   validation via readTree(). If parsing fails, fall back to logging
   the body as a safely quoted plain string.

This ensures the overall JSON log structure is always valid while
preserving existing behaviour for valid JSON bodies and keeping
the performance impact minimal for the common case.

Also resolves the long-standing TODO comment in
JsonHttpLogFormatterJackson2.prepareBody() regarding missing
JSON validation.

Test assertions in WriteBodyMaxSizeTest updated to reflect the
corrected behaviour: truncated bodies (produced by BodyMaxSizeFilter)
are now correctly logged as quoted strings rather than raw invalid JSON.

Fixes #2318

* Fix JSON formatter embedding non-JSON body as raw value

Extract shared JsonUtil validation helper per module. Add opt-in
validateJsonBody flag (default: false) to all four formatters and
wire it through LogbookProperties for Spring Boot users.

Fixes #2318

* Rename JsonUtil to JsonUtilJackson2 to fix duplicate-finder plugin

* Address review feedback: p... (continued)

874 of 922 branches covered (94.79%)

70 of 83 new or added lines in 7 files covered. (84.34%)

4007 of 4148 relevant lines covered (96.6%)

0.97 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.0
/logbook-json-jackson2/src/main/java/org/zalando/logbook/json/FastJsonHttpLogFormatterJackson2.java
1
package org.zalando.logbook.json;
2

3
import com.fasterxml.jackson.core.JsonFactory;
4
import com.fasterxml.jackson.core.JsonGenerator;
5
import com.fasterxml.jackson.databind.ObjectMapper;
6
import lombok.AllArgsConstructor;
7
import lombok.extern.slf4j.Slf4j;
8
import org.apiguardian.api.API;
9
import org.zalando.logbook.ContentType;
10
import org.zalando.logbook.Correlation;
11
import org.zalando.logbook.HttpLogFormatter;
12
import org.zalando.logbook.HttpMessage;
13
import org.zalando.logbook.HttpRequest;
14
import org.zalando.logbook.HttpResponse;
15
import org.zalando.logbook.Precorrelation;
16

17
import java.io.IOException;
18
import java.io.StringWriter;
19
import java.util.List;
20
import java.util.Map;
21

22
import static org.apiguardian.api.API.Status.STABLE;
23

24
/**
25
 * A custom {@link HttpLogFormatter} that produces JSON objects.
26
 */
27
@API(status = STABLE)
28
@Deprecated(since = "4.0.0", forRemoval = true)
29
@Slf4j
1✔
30
public final class FastJsonHttpLogFormatterJackson2 implements HttpLogFormatter {
31

32
    private final JsonFactory factory;
33

34
    private final JsonFieldWriterJackson2 delegate;
35
    
36
    private final boolean validateJsonBody;
37

38
    public FastJsonHttpLogFormatterJackson2() {
39
        this(new ObjectMapper());
1✔
40
    }
1✔
41

42
    public FastJsonHttpLogFormatterJackson2(final ObjectMapper mapper) {
43
        this(mapper, false);
1✔
44
    }
1✔
45

46
    public FastJsonHttpLogFormatterJackson2(final ObjectMapper mapper, final boolean validateJsonBody) {
47
        this(mapper.getFactory(), new DefaultJsonFieldWriter(mapper, validateJsonBody), validateJsonBody);
1✔
48
    }
1✔
49

50
    public FastJsonHttpLogFormatterJackson2(final ObjectMapper mapper, final JsonFieldWriterJackson2 writer) {
NEW
51
        this(mapper.getFactory(), writer, false);
×
NEW
52
    }
×
53

54
    public FastJsonHttpLogFormatterJackson2(final JsonFactory factory, final JsonFieldWriterJackson2 delegate) {
NEW
55
        this(factory, delegate, false);
×
NEW
56
    }
×
57

58
    public FastJsonHttpLogFormatterJackson2(final JsonFactory factory, final JsonFieldWriterJackson2 delegate, final boolean validateJsonBody) {
1✔
59
        this.factory = factory;
1✔
60
        this.delegate = delegate;
1✔
61
        this.validateJsonBody = validateJsonBody;
1✔
62
    }
1✔
63

64
    @FunctionalInterface
65
    private interface Formatter<C extends Precorrelation, H extends HttpMessage> {
66
        void format(C correlation, H message, JsonGenerator generator) throws IOException;
67
    }
68

69
    @Override
70
    public String format(
71
            final Precorrelation precorrelation,
72
            final HttpRequest request) throws IOException {
73

74
        return format(precorrelation, request, delegate::write);
1✔
75
    }
76

77
    @Override
78
    public String format(
79
            final Correlation correlation,
80
            final HttpResponse response) throws IOException {
81

82
        return format(correlation, response, delegate::write);
1✔
83
    }
84

85
    private <C extends Precorrelation, H extends HttpMessage> String format(
86
            final C correlation,
87
            final H message,
88
            final Formatter<C, H> formatter) throws IOException {
89

90
        final StringWriter writer = new StringWriter(message.getBody().length + 2048);
1✔
91

92
        try (final JsonGenerator generator = factory.createGenerator(writer)) {
1✔
93
            generator.writeStartObject();
1✔
94
            formatter.format(correlation, message, generator);
1✔
95
            delegate.write(message, generator);
1✔
96
            generator.writeEndObject();
1✔
97
        }
98

99
        return writer.toString();
1✔
100
    }
101

102
    private static class DefaultJsonFieldWriter implements JsonFieldWriterJackson2 {
103

104
        private final ObjectMapper mapper;
105
        private final boolean validateJsonBody;
106

107
        DefaultJsonFieldWriter(final ObjectMapper mapper) {
NEW
108
            this(mapper, false);
×
NEW
109
        }
×
110

111
        DefaultJsonFieldWriter(final ObjectMapper mapper, final boolean validateJsonBody) {
1✔
112
            this.mapper = mapper;
1✔
113
            this.validateJsonBody = validateJsonBody;
1✔
114
        }
1✔
115

116
        @Override
117
        public <M extends HttpMessage> void write(M message, JsonGenerator generator) throws IOException {
118
            writeHeaders(message, generator);
1✔
119
            writeBody(message, generator);
1✔
120
        }
1✔
121

122
        private void writeHeaders(
123
                final HttpMessage message,
124
                final JsonGenerator generator) throws IOException {
125

126
            final Map<String, List<String>> headers = message.getHeaders();
1✔
127

128
            if (headers.isEmpty()) {
1✔
129
                return;
1✔
130
            }
131

132
            // implementation note:
133
            // for some unclear reason, manually iterating over the headers
134
            // while writing performs worse than letting Jackson do the job.
135
            generator.writeObjectField("headers", headers);
1✔
136
        }
1✔
137

138
        private void writeBody(
139
                final HttpMessage message,
140
                final JsonGenerator generator) throws IOException {
141

142
            final String body = message.getBodyAsString();
1✔
143

144
            if (body.isEmpty()) {
1✔
145
                return;
1✔
146
            }
147
            generator.writeFieldName("body");
1✔
148

149
            final String contentType = message.getContentType();
1✔
150

151
            if (ContentType.isJsonMediaType(contentType)) {
1✔
152
                if (JsonUtilJackson2.looksLikeJson(body) && (!validateJsonBody || JsonUtilJackson2.isValidJson(body, mapper))) {
1!
153
                    generator.writeRawValue(body);
1✔
154
                } else {
155
                    generator.writeString(body);
1✔
156
                }
157
            } else {
158
                generator.writeString(body);
1✔
159
            }
160
        }
1✔
161
    }
162

163
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc