Repository: netty/netty
Issue: CorsHandler not responding with Content-Length
Labels: defect

### Expected behavior

* `CorsHandler` responds with `Content-Length: 0` when origin is allowed
* `CorsHandler` responds with `Content-Length: 0` when origin is not allowed

### Actual behavior

* `CorsHandler` responds with `Content-Length: 0` when origin is allowed
* `CorsHandler`  does not respond with a `Content-Length` header when origin is not allowed

### Steps to reproduce

* Run an `NioEventGroupLoop` with a `CorsHandler` in the pipeline. (see code)
* Make a request from an allowed origin, notice `Content-Length: 0` in the response. This is expected. (see code)
* Make a request from an origin that is not allowed, notice no `Content-Length` header in the response. This is unexpected. (see code)

### Minimal yet complete reproducer code (or URL to code)

Server side:

```java
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpMethod;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.cors.CorsConfigBuilder;
import io.netty.handler.codec.http.cors.CorsHandler;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

/**
 * Runs netty with a CORS handler on 8080
 */
public class NettyCorsBugApp {
    private static final int PORT = 8080;
    private static final String ALLOWED_ORIGIN = "http://example.com";

    public static void main(String[] args) throws Exception {
        EventLoopGroup eventLoopGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap bootstrap = new ServerBootstrap()
                    .group(eventLoopGroup)
                    .handler(new LoggingHandler(LogLevel.INFO))
                    .childHandler(new ChannelInitializer<Channel>() {
                        @Override
                        protected void initChannel(Channel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline();
                            pipeline.addLast(new HttpServerCodec());
                            pipeline.addLast(new HttpObjectAggregator(1024 * 1024)); // 1MB
                            pipeline.addLast(new CorsHandler(
                                    CorsConfigBuilder.forOrigin(ALLOWED_ORIGIN)
                                            .allowedRequestMethods(HttpMethod.POST)
                                            .build())
                            );
                        }
                    })
                    .channel(NioServerSocketChannel.class);
            Channel channel = bootstrap.bind(PORT).sync().channel();
            channel.closeFuture().sync();
        } finally {
            eventLoopGroup.shutdownGracefully();
        }
    }
}
```

Client side from an allowed origin. Notice how curl exits.

```
$ curl -sv -X OPTIONS -H 'Origin: http://example.com' -H 'Access-Control-Request-Method: POST' http://localhost:8080
* Rebuilt URL to: http://localhost:8080/
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> OPTIONS / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
> Origin: http://example.com
> Access-Control-Request-Method: POST
>
< HTTP/1.1 200 OK
< access-control-allow-origin: http://example.com
< vary: origin
< access-control-allow-methods: POST
< access-control-allow-headers:
< access-control-max-age: 0
< date: "Tue, 26 Sep 2017 20:03:53 GMT"
< content-length: 0
<
* Connection #0 to host localhost left intact
```

Client side from an origin that is not allowed. Notice how curl does not exit.

```
$ curl -sv -X OPTIONS -H 'Origin: http://invalid.com' -H 'Access-Control-Request-Method: POST' http://localhost:8080
* Rebuilt URL to: http://localhost:8080/
*   Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> OPTIONS / HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
> Origin: http://invalid.com
> Access-Control-Request-Method: POST
>
< HTTP/1.1 200 OK
* no chunk, no close, no size. Assume close to signal end
<
```

### Netty version

Tested `4.1.6-Final` and `4.1.16.Final`, same behavior.

### JVM version (e.g. `java -version`)

```
java version "1.8.0_131"
Java(TM) SE Runtime Environment (build 1.8.0_131-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.131-b11, mixed mode)
```

### OS version (e.g. `uname -a`)

```
Darwin WF14616 16.7.0 Darwin Kernel Version 16.7.0: Thu Jun 15 17:36:27 PDT 2017; root:xnu-3789.70.16~2/RELEASE_X86_64 x86_64
```

Issue comments:
@normanmaurer This looks related to https://github.com/netty/netty/commit/ecd6e5ce6d82756475e88ae69d2bfef1082edb68#diff-9cc075ed08ebc635ad5afcdb82bd449b I think we just need to add the `Content-Length` header for all cases, including when consumers use `preflightResponseHeader()` too.
Pull request for the fix, including unit tests: https://github.com/netty/netty/pull/7261
Fixed by https://github.com/netty/netty/pull/7261

Pull request review comments:
`HttpHeaderValues.ZERO`
`HtpHeaderValues.ZERO`
No problem, I changed that in commit 9c5c89c55. I squashed commits too so there is a single commit message and reverting is easy. If I shouldn't do that let me know.

Patch that resolved the issue:
diff --git a/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsHandler.java b/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsHandler.java
index 63bffe15f00..f4b5552b724 100644
--- a/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsHandler.java
+++ b/codec-http/src/main/java/io/netty/handler/codec/http/cors/CorsHandler.java
@@ -22,6 +22,7 @@
 import io.netty.channel.ChannelPromise;
 import io.netty.handler.codec.http.DefaultFullHttpResponse;
 import io.netty.handler.codec.http.HttpHeaderNames;
+import io.netty.handler.codec.http.HttpHeaderValues;
 import io.netty.handler.codec.http.HttpHeaders;
 import io.netty.handler.codec.http.HttpRequest;
 import io.netty.handler.codec.http.HttpResponse;
@@ -81,6 +82,9 @@ private void handlePreflight(final ChannelHandlerContext ctx, final HttpRequest
             setMaxAge(response);
             setPreflightHeaders(response);
         }
+        if (!response.headers().contains(HttpHeaderNames.CONTENT_LENGTH)) {
+            response.headers().set(HttpHeaderNames.CONTENT_LENGTH, HttpHeaderValues.ZERO);
+        }
         release(request);
         respond(ctx, request, response);
     }
@@ -205,8 +209,10 @@ public void write(final ChannelHandlerContext ctx, final Object msg, final Chann
     }
 
     private static void forbidden(final ChannelHandlerContext ctx, final HttpRequest request) {
+        HttpResponse response = new DefaultFullHttpResponse(request.protocolVersion(), FORBIDDEN);
+        response.headers().set(HttpHeaderNames.CONTENT_LENGTH, HttpHeaderValues.ZERO);
         release(request);
-        respond(ctx, request, new DefaultFullHttpResponse(request.protocolVersion(), FORBIDDEN));
+        respond(ctx, request, response);
     }
 
     private static void respond(


Test patch:
diff --git a/codec-http/src/test/java/io/netty/handler/codec/http/cors/CorsHandlerTest.java b/codec-http/src/test/java/io/netty/handler/codec/http/cors/CorsHandlerTest.java
index e3d67b6bf5a..f3fe97efd01 100644
--- a/codec-http/src/test/java/io/netty/handler/codec/http/cors/CorsHandlerTest.java
+++ b/codec-http/src/test/java/io/netty/handler/codec/http/cors/CorsHandlerTest.java
@@ -161,6 +161,7 @@ public void preflightRequestWithCustomHeader() {
         final HttpResponse response = preflightRequest(config, "http://localhost:8888", "content-type, xheader1");
         assertThat(response.headers().get(of("CustomHeader")), equalTo("somevalue"));
         assertThat(response.headers().get(VARY), equalTo(ORIGIN.toString()));
+        assertThat(response.headers().get(CONTENT_LENGTH), is("0"));
     }
 
     @Test
@@ -276,6 +277,7 @@ public void simpleRequestShortCircuit() {
         final CorsConfig config = forOrigin("http://localhost:8080").shortCircuit().build();
         final HttpResponse response = simpleRequest(config, "http://localhost:7777");
         assertThat(response.status(), is(FORBIDDEN));
+        assertThat(response.headers().get(CONTENT_LENGTH), is("0"));
     }
 
     @Test