micronaut-projects / micronaut-core

Micronaut Application Framework
http://micronaut.io
Apache License 2.0
6.06k stars 1.06k forks source link

HEAD responses are missing the content-length header #3685

Open avanishranjan opened 4 years ago

avanishranjan commented 4 years ago

Micronaut HEAD request is missing the content-length header in the response as opposed to GET.

@Controller
public class HelloController {

    @Get(value = "/hello", consumes = MediaType.APPLICATION_OCTET_STREAM, produces = MediaType.ALL)
    public byte[] hello(){
        String str1 = "aosdjfopsdjpojsdovjpsojdvpjspdvjpsjdv";
        return str1.getBytes();
    }
}

helloGetTest is passing while helloHeadTest is missing the content length.

@MicronautTest
public class HelloControllerTest {

    @Client("/")
    @Inject
    RxHttpClient client;

    @Test
    public void helloHeadTest(){
        HttpRequest<?> request = HttpRequest.HEAD("/hello")
            .accept(MediaType.APPLICATION_OCTET_STREAM_TYPE);

        HttpResponse<byte[]> response = client.toBlocking().exchange(request, byte[].class);
        assertEquals(HttpStatus.OK, response.getStatus());
        Assertions.assertNotNull(response.getHeaders().get("content-length"));
        Assertions.assertTrue(response.getHeaders().contentLength().getAsLong() == 37);

    }

    @Test
    public void helloGetTest(){
        HttpRequest<?> request = HttpRequest.GET("/hello")
            .accept(MediaType.APPLICATION_OCTET_STREAM_TYPE);

        HttpResponse<byte[]> response = client.toBlocking().exchange(request, byte[].class);

        assertEquals(HttpStatus.OK, response.getStatus());
        byte[] byteResponse = response.getBody().get();
        Assertions.assertTrue(byteResponse.length > 0);
        Assertions.assertNotNull(response.getHeaders().get("content-length"));
        Assertions.assertTrue(response.getHeaders().contentLength().getAsLong() == 37);
    }
}

Expected Behaviour

Both tests should pass and produce identical results

Actual Behaviour

Content-Length header is missing in the HEAD response.

Example Application

https://github.com/avanishranjan/mnheadbehavior/tree/master/complete

pditommaso commented 2 years ago

Also, the Content-Type is missing in the HEAD response

chrisparton1991 commented 2 years ago

My current workaround for this is to disable the built-in HEAD endpoints (e.g. @Get("/{id}", headRoute = false)), then define my own that calls my GET endpoint.

I'd love to be able to use Micronaut's built-in endpoint, but this does the job for now. I've got the HEAD endpoint logic in a reusable function.

@Head("/{id}")
fun head(
    auth: Authentication,
    @PathVariable id: String,
): HttpResponse<*> {
    val response = get(auth, id)
    val body = response.body()

    val contentLength = objectMapper.writeValueAsString(body).length.toLong()
    return HttpResponse
        .ok<Unit>()
        .contentType(response.contentType.orElse(null))
        .contentLength(contentLength)
}

@Get("/{id}", headRoute = false)
fun get(
    auth: Authentication,
    @PathVariable id: String,
): HttpResponse<*> {
    ...
}
alexanderankin commented 2 years ago

Ran into this while implementing a docker registry (educational purposes), docker daemon does a HEAD request and complains if content-type and content-length are empty on the response.

chrisparton1991 commented 2 years ago

Just realised there's a limitation in my code above.

Micronaut uses GZip encoding by default, which is reflected in the content-length returned in responses. My code is returning the length of the uncompressed JSON payload, so it will differ from the GET request.

Another approach I've tried is to implement an HTTP filter that intercepts HEAD requests, but the HTTP route matching seems to occur before any filters are run, which rules out that idea.

pditommaso commented 2 years ago

I think this problem has been solved by this PR https://github.com/micronaut-projects/micronaut-core/pull/6903

yawkat commented 2 years ago

@pditommaso unfortunately not

chrisparton1991 commented 1 year ago

I ended up adding my own HEAD endpoints, which inspect the HttpRequest to make a local call to the corresponding GET endpoint (http://127.0.0.1:${embeddedServer.port}/<request URL>).

That allows me to retrieve the content length and any other headers, and return them as part of the HEAD response.

This is a nasty hack and I'm far from proud of it, but it works.