micronaut-core icon indicating copy to clipboard operation
micronaut-core copied to clipboard

HEAD responses are missing the content-length header

Open avanishranjan opened this issue 3 years ago • 6 comments

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

avanishranjan avatar Jul 08 '20 15:07 avanishranjan

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

pditommaso avatar Jan 28 '22 21:01 pditommaso

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<*> {
    ...
}

chrisparton1991 avatar Apr 01 '22 01:04 chrisparton1991

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.

alexanderankin avatar Jun 12 '22 00:06 alexanderankin

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.

chrisparton1991 avatar Jul 11 '22 03:07 chrisparton1991

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

pditommaso avatar Jul 11 '22 06:07 pditommaso

@pditommaso unfortunately not

yawkat avatar Jul 11 '22 08:07 yawkat

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.

chrisparton1991 avatar Dec 08 '22 22:12 chrisparton1991