Repository: zalando/logbook (java)

Issue:
Detect correct protocol version
The `DefaultHttpLogFormatter` silently assumes `HTTP/1.1`. We should use the correct APIs for that:
- `ServletRequest.getProtocol`
- `HttpRequest.getRequestLine().getProtocol()`

Pull request description:
Added support for protocol version
This fixes #75

Patch:
diff --git a/README.md b/README.md
index 51bce554..b8faacfa 100644
--- a/README.md
+++ b/README.md
@@ -175,6 +175,7 @@ Content-Type: application/json
   "origin": "remote",
   "type": "request",
   "correlation": "2d66e4bc-9a0d-11e5-a84c-1f39510f0d6b",
+  "protocol": "HTTP/1.1",
   "sender": "127.0.0.1",
   "method": "GET",
   "path": "http://example.org/test",
@@ -196,6 +197,7 @@ Content-Type: application/json
   "origin": "local",
   "type": "response",
   "correlation": "2d66e4bc-9a0d-11e5-a84c-1f39510f0d6b",
+  "protocol": "HTTP/1.1",
   "status": 200,
   "headers": {
     "Content-Type": ["text/plain"]
@@ -212,6 +214,7 @@ a JSON response body will **not** be escaped and represented as a string:
   "origin": "local",
   "type": "response",
   "correlation": "2d66e4bc-9a0d-11e5-a84c-1f39510f0d6b",
+  "protocol": "HTTP/1.1",
   "status": 200,
   "headers": {
     "Content-Type": ["application/json"]
diff --git a/logbook-api/src/main/java/org/zalando/logbook/BaseHttpMessage.java b/logbook-api/src/main/java/org/zalando/logbook/BaseHttpMessage.java
index a02d4e27..2e1dea5b 100644
--- a/logbook-api/src/main/java/org/zalando/logbook/BaseHttpMessage.java
+++ b/logbook-api/src/main/java/org/zalando/logbook/BaseHttpMessage.java
@@ -31,13 +31,15 @@ import static java.lang.String.CASE_INSENSITIVE_ORDER;
 
 public interface BaseHttpMessage {
 
+    String getProtocolVersion();
+
+    Origin getOrigin();
+
     ListMultimap<String, String> getHeaders();
 
     String getContentType();
 
     Charset getCharset();
-    
-    Origin getOrigin();
 
     class Headers {
 
diff --git a/logbook-api/src/main/java/org/zalando/logbook/BodyObfuscator.java b/logbook-api/src/main/java/org/zalando/logbook/BodyObfuscator.java
index 968b58af..dfe1a2f1 100644
--- a/logbook-api/src/main/java/org/zalando/logbook/BodyObfuscator.java
+++ b/logbook-api/src/main/java/org/zalando/logbook/BodyObfuscator.java
@@ -29,7 +29,4 @@ public interface BodyObfuscator {
         return (contentType, body) -> body;
     }
 
-    // TODO a special BodyObfuscator that only works for application/x-www-form-urlencoded and delegates to a key-value
-    // based obfuscator + Query Parameters
-
 }
diff --git a/logbook-core/src/main/java/org/zalando/logbook/DefaultHttpLogFormatter.java b/logbook-core/src/main/java/org/zalando/logbook/DefaultHttpLogFormatter.java
index 1dae0b98..d8a17e98 100644
--- a/logbook-core/src/main/java/org/zalando/logbook/DefaultHttpLogFormatter.java
+++ b/logbook-core/src/main/java/org/zalando/logbook/DefaultHttpLogFormatter.java
@@ -42,7 +42,7 @@ public final class DefaultHttpLogFormatter implements HttpLogFormatter {
     }
 
     private String formatRequestLine(final HttpRequest request) {
-        return String.format("%s %s HTTP/1.1", request.getMethod(), renderRequestUri(request));
+        return String.format("%s %s %s", request.getMethod(), renderRequestUri(request), request.getProtocolVersion());
     }
 
     private String renderRequestUri(HttpRequest request) {
@@ -58,7 +58,7 @@ public final class DefaultHttpLogFormatter implements HttpLogFormatter {
 
     private String formatStatusLine(final HttpResponse response) {
         // TODO we are missing the reason phrase here, e.g. "OK", but there is no complete list in the JDK alone
-        return String.format("HTTP/1.1 %d", response.getStatus());
+        return String.format("%s %d", response.getProtocolVersion(), response.getStatus());
     }
 
     private <H extends HttpMessage> String format(final H message, final String type, final String correlationId,
diff --git a/logbook-core/src/main/java/org/zalando/logbook/ForwardingHttpMessage.java b/logbook-core/src/main/java/org/zalando/logbook/ForwardingHttpMessage.java
index 2259ce57..abdf65e6 100644
--- a/logbook-core/src/main/java/org/zalando/logbook/ForwardingHttpMessage.java
+++ b/logbook-core/src/main/java/org/zalando/logbook/ForwardingHttpMessage.java
@@ -31,6 +31,11 @@ public abstract class ForwardingHttpMessage extends ForwardingObject implements
     @Override
     protected abstract HttpMessage delegate();
 
+    @Override
+    public String getProtocolVersion() {
+        return delegate().getProtocolVersion();
+    }
+
     @Override
     public ListMultimap<String, String> getHeaders() {
         return delegate().getHeaders();
diff --git a/logbook-core/src/main/java/org/zalando/logbook/ForwardingRawHttpRequest.java b/logbook-core/src/main/java/org/zalando/logbook/ForwardingRawHttpRequest.java
index f65b23e8..ba193cd7 100644
--- a/logbook-core/src/main/java/org/zalando/logbook/ForwardingRawHttpRequest.java
+++ b/logbook-core/src/main/java/org/zalando/logbook/ForwardingRawHttpRequest.java
@@ -71,6 +71,11 @@ public abstract class ForwardingRawHttpRequest extends ForwardingObject implemen
         return delegate().getQueryParameters();
     }
 
+    @Override
+    public String getProtocolVersion() {
+        return delegate().getProtocolVersion();
+    }
+
     @Override
     public HttpRequest withBody() throws IOException {
         return delegate().withBody();
diff --git a/logbook-core/src/main/java/org/zalando/logbook/JsonHttpLogFormatter.java b/logbook-core/src/main/java/org/zalando/logbook/JsonHttpLogFormatter.java
index 9c5cdd8e..a725cfda 100644
--- a/logbook-core/src/main/java/org/zalando/logbook/JsonHttpLogFormatter.java
+++ b/logbook-core/src/main/java/org/zalando/logbook/JsonHttpLogFormatter.java
@@ -64,6 +64,7 @@ public final class JsonHttpLogFormatter implements HttpLogFormatter {
         builder.put("origin", translate(request.getOrigin()));
         builder.put("type", "request");
         builder.put("correlation", correlationId);
+        builder.put("protocol", request.getProtocolVersion());
         builder.put("remote", request.getRemote());
         builder.put("method", request.getMethod());
         builder.put("uri", renderRequestUri(request));
@@ -91,6 +92,7 @@ public final class JsonHttpLogFormatter implements HttpLogFormatter {
         builder.put("origin", translate(response.getOrigin()));
         builder.put("type", "response");
         builder.put("correlation", correlationId);
+        builder.put("protocol", response.getProtocolVersion());
         builder.put("status", response.getStatus());
         addUnless(builder, "headers", response.getHeaders().asMap(), Map::isEmpty);
         addBody(response, builder);
diff --git a/logbook-httpclient/src/main/java/org/zalando/logbook/httpclient/LocalRequest.java b/logbook-httpclient/src/main/java/org/zalando/logbook/httpclient/LocalRequest.java
index 40a54895..2aed96af 100644
--- a/logbook-httpclient/src/main/java/org/zalando/logbook/httpclient/LocalRequest.java
+++ b/logbook-httpclient/src/main/java/org/zalando/logbook/httpclient/LocalRequest.java
@@ -78,6 +78,11 @@ final class LocalRequest implements RawHttpRequest, org.zalando.logbook.HttpRequ
         return Origin.LOCAL;
     }
 
+    @Override
+    public String getProtocolVersion() {
+        return request.getRequestLine().getProtocolVersion().toString();
+    }
+
     @Override
     public String getRemote() {
         try {
@@ -95,17 +100,17 @@ final class LocalRequest implements RawHttpRequest, org.zalando.logbook.HttpRequ
     @Override
     public String getRequestUri() {
         return stripQueryString(
-                originalRequestUri.getScheme(), 
-                originalRequestUri.getUserInfo(), 
-                originalRequestUri.getHost(), 
-                originalRequestUri.getPort(), 
-                originalRequestUri.getPath(), 
+                originalRequestUri.getScheme(),
+                originalRequestUri.getUserInfo(),
+                originalRequestUri.getHost(),
+                originalRequestUri.getPort(),
+                originalRequestUri.getPath(),
                 originalRequestUri.getFragment());
     }
 
     @SneakyThrows
     @VisibleForTesting
-    static String stripQueryString(final String scheme, final String userInfo, final String host, final int port, 
+    static String stripQueryString(final String scheme, final String userInfo, final String host, final int port,
             final String path, final String fragment) {
         return new URI(scheme, userInfo, host, port, path, null, fragment).toASCIIString();
     }
@@ -115,15 +120,15 @@ final class LocalRequest implements RawHttpRequest, org.zalando.logbook.HttpRequ
         final ListMultimap<String, String> parameters = ArrayListMultimap.create();
 
         @Nullable final String query = originalRequestUri.getRawQuery();
-        
+
         if (query == null) {
             return ImmutableListMultimap.of();
         }
-        
+
         for (NameValuePair pair : parse(query, UTF_8)) {
             parameters.put(pair.getName(), pair.getValue());
         }
-        
+
         return Multimaps.unmodifiableListMultimap(parameters);
     }
 
diff --git a/logbook-httpclient/src/main/java/org/zalando/logbook/httpclient/RemoteResponse.java b/logbook-httpclient/src/main/java/org/zalando/logbook/httpclient/RemoteResponse.java
index eb03383c..4d42fcd8 100644
--- a/logbook-httpclient/src/main/java/org/zalando/logbook/httpclient/RemoteResponse.java
+++ b/logbook-httpclient/src/main/java/org/zalando/logbook/httpclient/RemoteResponse.java
@@ -51,6 +51,11 @@ final class RemoteResponse implements RawHttpResponse, org.zalando.logbook.HttpR
         return Origin.REMOTE;
     }
 
+    @Override
+    public String getProtocolVersion() {
+        return response.getStatusLine().getProtocolVersion().toString();
+    }
+
     @Override
     public int getStatus() {
         return response.getStatusLine().getStatusCode();
diff --git a/logbook-servlet/src/main/java/org/zalando/logbook/servlet/LocalResponse.java b/logbook-servlet/src/main/java/org/zalando/logbook/servlet/LocalResponse.java
index cebe2cf2..1de4cb63 100644
--- a/logbook-servlet/src/main/java/org/zalando/logbook/servlet/LocalResponse.java
+++ b/logbook-servlet/src/main/java/org/zalando/logbook/servlet/LocalResponse.java
@@ -49,6 +49,7 @@ final class LocalResponse extends HttpServletResponseWrapper implements RawHttpR
 
     private final TeeServletOutputStream stream;
     private final PrintWriter writer;
+    private final String protocolVersion;
 
     /**
      * Null until we successfully intercepted it.
@@ -56,10 +57,11 @@ final class LocalResponse extends HttpServletResponseWrapper implements RawHttpR
     @Nullable
     private byte[] body;
 
-    LocalResponse(final HttpServletResponse response) throws IOException {
+    LocalResponse(final HttpServletResponse response, final String protocolVersion) throws IOException {
         super(response);
         this.stream = new TeeServletOutputStream();
         this.writer = new TeePrintWriter();
+        this.protocolVersion = protocolVersion;
     }
 
     @Override
@@ -67,6 +69,11 @@ final class LocalResponse extends HttpServletResponseWrapper implements RawHttpR
         return Origin.LOCAL;
     }
 
+    @Override
+    public String getProtocolVersion() {
+        return protocolVersion;
+    }
+
     @Override
     public ListMultimap<String, String> getHeaders() {
         final ListMultimap<String, String> headers = Headers.create();
diff --git a/logbook-servlet/src/main/java/org/zalando/logbook/servlet/NormalStrategy.java b/logbook-servlet/src/main/java/org/zalando/logbook/servlet/NormalStrategy.java
index b35ecd88..a6c324df 100644
--- a/logbook-servlet/src/main/java/org/zalando/logbook/servlet/NormalStrategy.java
+++ b/logbook-servlet/src/main/java/org/zalando/logbook/servlet/NormalStrategy.java
@@ -43,7 +43,8 @@ final class NormalStrategy implements Strategy {
         final Optional<Correlator> correlator = logRequestIfNecessary(logbook, request);
 
         if (correlator.isPresent()) {
-            final LocalResponse response = new LocalResponse(httpResponse);
+            final String protocolVersion = request.getProtocolVersion();
+            final LocalResponse response = new LocalResponse(httpResponse, protocolVersion);
 
             chain.doFilter(request, response);
             response.getWriter().flush();
diff --git a/logbook-servlet/src/main/java/org/zalando/logbook/servlet/RemoteRequest.java b/logbook-servlet/src/main/java/org/zalando/logbook/servlet/RemoteRequest.java
index ab6f94a2..5470fc30 100644
--- a/logbook-servlet/src/main/java/org/zalando/logbook/servlet/RemoteRequest.java
+++ b/logbook-servlet/src/main/java/org/zalando/logbook/servlet/RemoteRequest.java
@@ -63,6 +63,11 @@ final class RemoteRequest extends HttpServletRequestWrapper implements RawHttpRe
         super(request);
     }
 
+    @Override
+    public String getProtocolVersion() {
+        return getProtocol();
+    }
+
     @Override
     public Origin getOrigin() {
         return Origin.REMOTE;
@@ -81,10 +86,10 @@ final class RemoteRequest extends HttpServletRequestWrapper implements RawHttpRe
     @Override
     public ListMultimap<String, String> getQueryParameters() {
         final ListMultimap<String, String> parameters = ArrayListMultimap.create();
-        
-        getParameterMap().forEach((name, values) -> 
+
+        getParameterMap().forEach((name, values) ->
                 Collections.addAll(parameters.get(name), values));
-        
+
         return Multimaps.unmodifiableListMultimap(parameters);
     }
 
diff --git a/logbook-servlet/src/main/java/org/zalando/logbook/servlet/SecurityStrategy.java b/logbook-servlet/src/main/java/org/zalando/logbook/servlet/SecurityStrategy.java
index 212b4d78..546ede43 100644
--- a/logbook-servlet/src/main/java/org/zalando/logbook/servlet/SecurityStrategy.java
+++ b/logbook-servlet/src/main/java/org/zalando/logbook/servlet/SecurityStrategy.java
@@ -39,7 +39,8 @@ final class SecurityStrategy implements Strategy {
             final HttpServletResponse httpResponse, final FilterChain chain) throws ServletException, IOException {
 
         final RemoteRequest request = new RemoteRequest(httpRequest);
-        final LocalResponse response = new LocalResponse(httpResponse);
+        final String protocolVersion = request.getProtocolVersion();
+        final LocalResponse response = new LocalResponse(httpResponse, protocolVersion);
 
         chain.doFilter(request, response);
 


Test patch:
diff --git a/logbook-core/src/test/java/org/zalando/logbook/DefaultHttpLogFormatterTest.java b/logbook-core/src/test/java/org/zalando/logbook/DefaultHttpLogFormatterTest.java
index 52d84113..e7022b92 100644
--- a/logbook-core/src/test/java/org/zalando/logbook/DefaultHttpLogFormatterTest.java
+++ b/logbook-core/src/test/java/org/zalando/logbook/DefaultHttpLogFormatterTest.java
@@ -38,6 +38,7 @@ public final class DefaultHttpLogFormatterTest {
     public void shouldLogRequest() throws IOException {
         final String correlationId = "c9408eaa-677d-11e5-9457-10ddb1ee7671";
         final HttpRequest request = MockHttpRequest.builder()
+                .protocolVersion("HTTP/1.0")
                 .origin(Origin.REMOTE)
                 .requestUri("/test")
                 .queryParameters(ImmutableListMultimap.of(
@@ -52,7 +53,7 @@ public final class DefaultHttpLogFormatterTest {
         final String http = unit.format(new SimplePrecorrelation<>(correlationId, request));
 
         assertThat(http, equalTo("Incoming Request: c9408eaa-677d-11e5-9457-10ddb1ee7671\n" +
-                "GET /test?limit=1 HTTP/1.1\n" +
+                "GET /test?limit=1 HTTP/1.0\n" +
                 "Accept: application/json\n" +
                 "Content-Type: text/plain\n" +
                 "\n" +
@@ -101,6 +102,7 @@ public final class DefaultHttpLogFormatterTest {
         final String correlationId = "2d51bc02-677e-11e5-8b9b-10ddb1ee7671";
         final HttpRequest request = MockHttpRequest.create();
         final HttpResponse response = MockHttpResponse.builder()
+                .protocolVersion("HTTP/1.0")
                 .origin(Origin.REMOTE)
                 .headers(ImmutableListMultimap.of("Content-Type", "application/json"))
                 .body("{\"success\":true}")
@@ -109,7 +111,7 @@ public final class DefaultHttpLogFormatterTest {
         final String http = unit.format(new SimpleCorrelation<>(correlationId, request, response));
 
         assertThat(http, equalTo("Incoming Response: 2d51bc02-677e-11e5-8b9b-10ddb1ee7671\n" +
-                "HTTP/1.1 200\n" +
+                "HTTP/1.0 200\n" +
                 "Content-Type: application/json\n" +
                 "\n" +
                 "{\"success\":true}"));
diff --git a/logbook-core/src/test/java/org/zalando/logbook/ForwardingHttpRequestTest.java b/logbook-core/src/test/java/org/zalando/logbook/ForwardingHttpRequestTest.java
index 01c2c0ef..82026ff9 100644
--- a/logbook-core/src/test/java/org/zalando/logbook/ForwardingHttpRequestTest.java
+++ b/logbook-core/src/test/java/org/zalando/logbook/ForwardingHttpRequestTest.java
@@ -47,6 +47,7 @@ public final class ForwardingHttpRequestTest {
         assertThat(unit.getMethod(), is("GET"));
         assertThat(unit.getRequestUri(), is("http://localhost/"));
         assertThat(unit.getQueryParameters().values(), is(empty()));
+        assertThat(unit.getProtocolVersion(), is("HTTP/1.1"));
         assertThat(unit.getHeaders().values(), is(empty()));
         assertThat(unit.getContentType(), is(""));
         assertThat(unit.getCharset(), is(UTF_8));
diff --git a/logbook-core/src/test/java/org/zalando/logbook/ForwardingRawHttpRequestTest.java b/logbook-core/src/test/java/org/zalando/logbook/ForwardingRawHttpRequestTest.java
index a8e70391..dadc4507 100644
--- a/logbook-core/src/test/java/org/zalando/logbook/ForwardingRawHttpRequestTest.java
+++ b/logbook-core/src/test/java/org/zalando/logbook/ForwardingRawHttpRequestTest.java
@@ -50,6 +50,7 @@ public final class ForwardingRawHttpRequestTest {
         assertThat(unit.getMethod(), is("GET"));
         assertThat(unit.getRequestUri(), is("http://localhost/"));
         assertThat(unit.getQueryParameters().values(), is(empty()));
+        assertThat(unit.getProtocolVersion(), is("HTTP/1.1"));
         assertThat(unit.getHeaders(), is(ImmutableMultimap.of()));
         assertThat(unit.getContentType(), is(""));
         assertThat(unit.getCharset(), is(UTF_8));
@@ -64,6 +65,7 @@ public final class ForwardingRawHttpRequestTest {
         assertThat(request.getMethod(), is("GET"));
         assertThat(request.getRequestUri(), is("http://localhost/"));
         assertThat(request.getQueryParameters().values(), is(empty()));
+        assertThat(request.getProtocolVersion(), is("HTTP/1.1"));
         assertThat(request.getHeaders().values(), is(empty()));
         assertThat(request.getContentType(), is(""));
         assertThat(request.getCharset(), is(UTF_8));
diff --git a/logbook-core/src/test/java/org/zalando/logbook/JsonHttpLogFormatterTest.java b/logbook-core/src/test/java/org/zalando/logbook/JsonHttpLogFormatterTest.java
index 0ee461d0..1eb00a6b 100644
--- a/logbook-core/src/test/java/org/zalando/logbook/JsonHttpLogFormatterTest.java
+++ b/logbook-core/src/test/java/org/zalando/logbook/JsonHttpLogFormatterTest.java
@@ -47,6 +47,7 @@ public final class JsonHttpLogFormatterTest {
     public void shouldLogRequest() throws IOException {
         final String correlationId = "3ce91230-677b-11e5-87b7-10ddb1ee7671";
         final HttpRequest request = MockHttpRequest.builder()
+                .protocolVersion("HTTP/1.0")
                 .origin(REMOTE)
                 .requestUri("/test")
                 .queryParameters(ImmutableListMultimap.of(
@@ -65,6 +66,7 @@ public final class JsonHttpLogFormatterTest {
                 .assertThat("$.origin", is("remote"))
                 .assertThat("$.type", is("request"))
                 .assertThat("$.correlation", is("3ce91230-677b-11e5-87b7-10ddb1ee7671"))
+                .assertThat("$.protocol", is("HTTP/1.0"))
                 .assertThat("$.remote", is("127.0.0.1"))
                 .assertThat("$.method", is("GET"))
                 .assertThat("$.uri", is("/test?limit=1"))
@@ -188,6 +190,7 @@ public final class JsonHttpLogFormatterTest {
         final String correlationId = "53de2640-677d-11e5-bc84-10ddb1ee7671";
         final HttpRequest request = MockHttpRequest.create();
         final HttpResponse response = MockHttpResponse.builder()
+                .protocolVersion("HTTP/1.0")
                 .origin(LOCAL)
                 .headers(ImmutableListMultimap.of("Date", "Tue, 15 Nov 1994 08:12:31 GMT"))
                 .contentType("application/xml")
@@ -200,6 +203,7 @@ public final class JsonHttpLogFormatterTest {
                 .assertThat("$.origin", is("local"))
                 .assertThat("$.type", is("response"))
                 .assertThat("$.correlation", is("53de2640-677d-11e5-bc84-10ddb1ee7671"))
+                .assertThat("$.protocol", is("HTTP/1.0"))
                 .assertThat("$.status", is(200))
                 .assertThat("$.headers.*", hasSize(1))
                 .assertThat("$.headers['Date']", is(singletonList("Tue, 15 Nov 1994 08:12:31 GMT")))
diff --git a/logbook-core/src/test/java/org/zalando/logbook/QueryParametersTest.java b/logbook-core/src/test/java/org/zalando/logbook/QueryParametersTest.java
index 8f66e02b..21241845 100644
--- a/logbook-core/src/test/java/org/zalando/logbook/QueryParametersTest.java
+++ b/logbook-core/src/test/java/org/zalando/logbook/QueryParametersTest.java
@@ -60,6 +60,4 @@ public final class QueryParametersTest {
         assertThat(render(of("c", "3", "d", "4", "a", "1", "e", "5", "b", "2")), is("c=3&d=4&a=1&e=5&b=2"));
     }
 
-    // TODO test decode/encode
-
 }
\ No newline at end of file
diff --git a/logbook-servlet/src/test/java/org/zalando/logbook/servlet/WritingTest.java b/logbook-servlet/src/test/java/org/zalando/logbook/servlet/WritingTest.java
index f3d739d1..9c0eb48b 100644
--- a/logbook-servlet/src/test/java/org/zalando/logbook/servlet/WritingTest.java
+++ b/logbook-servlet/src/test/java/org/zalando/logbook/servlet/WritingTest.java
@@ -25,6 +25,7 @@ import org.junit.Test;
 import org.mockito.ArgumentCaptor;
 import org.springframework.http.MediaType;
 import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.request.RequestPostProcessor;
 import org.springframework.test.web.servlet.setup.MockMvcBuilders;
 import org.zalando.logbook.Correlation;
 import org.zalando.logbook.DefaultHttpLogFormatter;
@@ -73,6 +74,7 @@ public final class WritingTest {
     @Test
     public void shouldLogRequest() throws Exception {
         mvc.perform(get("/api/sync")
+                .with(protocol("HTTP/1.1"))
                 .accept(MediaType.APPLICATION_JSON)
                 .header("Host", "localhost")
                 .contentType(MediaType.TEXT_PLAIN)
@@ -86,16 +88,17 @@ public final class WritingTest {
         assertThat(precorrelation.getRequest(), startsWith("Incoming Request:"));
         assertThat(precorrelation.getRequest(), endsWith(
                 "GET http://localhost/api/sync HTTP/1.1\n" +
-                "Accept: application/json\n" +
-                "Host: localhost\n" +
-                "Content-Type: text/plain\n" +
-                "\n" +
-                "Hello, world!"));
+                        "Accept: application/json\n" +
+                        "Host: localhost\n" +
+                        "Content-Type: text/plain\n" +
+                        "\n" +
+                        "Hello, world!"));
     }
 
     @Test
     public void shouldLogResponse() throws Exception {
-        mvc.perform(get("/api/sync"));
+        mvc.perform(get("/api/sync")
+                .with(protocol("HTTP/1.1")));
 
         @SuppressWarnings("unchecked")
         final ArgumentCaptor<Correlation<String, String>> captor = ArgumentCaptor.forClass(Correlation.class);
@@ -105,9 +108,16 @@ public final class WritingTest {
         assertThat(correlation.getResponse(), startsWith("Outgoing Response:"));
         assertThat(correlation.getResponse(), endsWith(
                 "HTTP/1.1 200\n" +
-                "Content-Type: application/json\n" +
-                "\n" +
-                "{\"value\":\"Hello, world!\"}"));
+                        "Content-Type: application/json\n" +
+                        "\n" +
+                        "{\"value\":\"Hello, world!\"}"));
+    }
+
+    private RequestPostProcessor protocol(String protocol) {
+        return request -> {
+            request.setProtocol(protocol);
+            return request;
+        };
     }
 
 }
diff --git a/logbook-test/src/main/java/org/zalando/logbook/MockHttpRequest.java b/logbook-test/src/main/java/org/zalando/logbook/MockHttpRequest.java
index 1006db30..ece157a7 100644
--- a/logbook-test/src/main/java/org/zalando/logbook/MockHttpRequest.java
+++ b/logbook-test/src/main/java/org/zalando/logbook/MockHttpRequest.java
@@ -33,6 +33,7 @@ import static org.zalando.logbook.BaseHttpMessage.Headers.copy;
 
 public final class MockHttpRequest implements MockHttpMessage, HttpRequest {
 
+    private final String protocolVersion;
     private final Origin origin;
     private final String remote;
     private final String method;
@@ -44,7 +45,9 @@ public final class MockHttpRequest implements MockHttpMessage, HttpRequest {
     private final String body;
 
     @Builder
-    public MockHttpRequest(@Nullable final Origin origin, 
+    public MockHttpRequest(
+            @Nullable final String protocolVersion,
+            @Nullable final Origin origin,
             @Nullable final String remote,
             @Nullable final String method,
             @Nullable final String requestUri,
@@ -53,6 +56,7 @@ public final class MockHttpRequest implements MockHttpMessage, HttpRequest {
             @Nullable final String contentType,
             @Nullable final Charset charset,
             @Nullable final String body) {
+        this.protocolVersion = firstNonNull(protocolVersion, "HTTP/1.1");
         this.origin = firstNonNull(origin, Origin.REMOTE);
         this.remote = firstNonNull(remote, "127.0.0.1");
         this.method = firstNonNull(method, "GET");
@@ -64,6 +68,11 @@ public final class MockHttpRequest implements MockHttpMessage, HttpRequest {
         this.body = firstNonNull(body, "");
     }
 
+    @Override
+    public String getProtocolVersion() {
+        return protocolVersion;
+    }
+
     @Override
     public Origin getOrigin() {
         return origin;
diff --git a/logbook-test/src/main/java/org/zalando/logbook/MockHttpResponse.java b/logbook-test/src/main/java/org/zalando/logbook/MockHttpResponse.java
index ff2b9628..ca005b0d 100644
--- a/logbook-test/src/main/java/org/zalando/logbook/MockHttpResponse.java
+++ b/logbook-test/src/main/java/org/zalando/logbook/MockHttpResponse.java
@@ -33,6 +33,7 @@ import static org.zalando.logbook.BaseHttpMessage.Headers.copy;
 
 public final class MockHttpResponse implements MockHttpMessage, HttpResponse {
 
+    private final String protocolVersion;
     private final Origin origin;
     private final int status;
     private final ListMultimap<String, String> headers;
@@ -41,12 +42,15 @@ public final class MockHttpResponse implements MockHttpMessage, HttpResponse {
     private final String body;
 
     @Builder
-    public MockHttpResponse(@Nullable final Origin origin, 
+    public MockHttpResponse(
+            @Nullable final String protocolVersion,
+            @Nullable final Origin origin,
             final int status,
             @Nullable final ListMultimap<String, String> headers,
             @Nullable final String contentType,
             @Nullable final Charset charset,
             @Nullable final String body) {
+        this.protocolVersion = firstNonNull(protocolVersion, "HTTP/1.1");
         this.origin = firstNonNull(origin, Origin.LOCAL);
         this.status = status == 0 ? 200 : status;
         this.headers = copy(firstNonNullNorEmpty(headers, ImmutableListMultimap.of()));
@@ -55,6 +59,11 @@ public final class MockHttpResponse implements MockHttpMessage, HttpResponse {
         this.body = firstNonNull(body, "");
     }
 
+    @Override
+    public String getProtocolVersion() {
+        return protocolVersion;
+    }
+
     @Override
     public Origin getOrigin() {
         return origin;
diff --git a/logbook-test/src/main/java/org/zalando/logbook/MockRawHttpRequest.java b/logbook-test/src/main/java/org/zalando/logbook/MockRawHttpRequest.java
index d8ee2dfa..33ee3086 100644
--- a/logbook-test/src/main/java/org/zalando/logbook/MockRawHttpRequest.java
+++ b/logbook-test/src/main/java/org/zalando/logbook/MockRawHttpRequest.java
@@ -34,6 +34,7 @@ import static org.zalando.logbook.BaseHttpMessage.Headers.copy;
 
 public final class MockRawHttpRequest implements MockHttpMessage, RawHttpRequest {
 
+    private final String protocolVersion;
     private final Origin origin;
     private final String remote;
     private final String method;
@@ -44,7 +45,9 @@ public final class MockRawHttpRequest implements MockHttpMessage, RawHttpRequest
     private final Charset charset;
 
     @Builder
-    public MockRawHttpRequest(@Nullable final Origin origin, 
+    public MockRawHttpRequest(
+            @Nullable final String protocolVersion,
+            @Nullable final Origin origin,
             @Nullable final String remote,
             @Nullable final String method,
             @Nullable final String requestUri,
@@ -52,6 +55,7 @@ public final class MockRawHttpRequest implements MockHttpMessage, RawHttpRequest
             @Nullable final ListMultimap<String, String> headers,
             @Nullable final String contentType,
             @Nullable final Charset charset) {
+        this.protocolVersion = firstNonNull(protocolVersion, "HTTP/1.1");
         this.origin = firstNonNull(origin, Origin.REMOTE);
         this.remote = firstNonNull(remote, "127.0.0.1");
         this.method = firstNonNull(method, "GET");
@@ -62,6 +66,11 @@ public final class MockRawHttpRequest implements MockHttpMessage, RawHttpRequest
         this.charset = firstNonNull(charset, StandardCharsets.UTF_8);
     }
 
+    @Override
+    public String getProtocolVersion() {
+        return protocolVersion;
+    }
+
     @Override
     public String getRemote() {
         return remote;
diff --git a/logbook-test/src/main/java/org/zalando/logbook/MockRawHttpResponse.java b/logbook-test/src/main/java/org/zalando/logbook/MockRawHttpResponse.java
index e39828c5..5442080e 100644
--- a/logbook-test/src/main/java/org/zalando/logbook/MockRawHttpResponse.java
+++ b/logbook-test/src/main/java/org/zalando/logbook/MockRawHttpResponse.java
@@ -34,6 +34,7 @@ import static org.zalando.logbook.BaseHttpMessage.Headers.copy;
 
 public final class MockRawHttpResponse implements MockHttpMessage, RawHttpResponse {
 
+    private final String protocolVersion;
     private final Origin origin;
     private final int status;
     private final ListMultimap<String, String> headers;
@@ -41,11 +42,14 @@ public final class MockRawHttpResponse implements MockHttpMessage, RawHttpRespon
     private final Charset charset;
 
     @Builder
-    public MockRawHttpResponse(@Nullable final Origin origin,
+    public MockRawHttpResponse(
+            @Nullable final String protocolVersion,
+            @Nullable final Origin origin,
             final int status,
             @Nullable final ListMultimap<String, String> headers,
             @Nullable final String contentType,
             @Nullable final Charset charset) {
+        this.protocolVersion = firstNonNull(protocolVersion, "HTTP/1.1");
         this.origin = firstNonNull(origin, Origin.REMOTE);
         this.status = status == 0 ? 200 : status;
         this.headers = copy(firstNonNullNorEmpty(headers, ImmutableListMultimap.of()));
@@ -53,6 +57,11 @@ public final class MockRawHttpResponse implements MockHttpMessage, RawHttpRespon
         this.charset = firstNonNull(charset, StandardCharsets.UTF_8);
     }
 
+    @Override
+    public String getProtocolVersion() {
+        return protocolVersion;
+    }
+
     @Override
     public int getStatus() {
         return status;