micronaut-core
micronaut-core copied to clipboard
HEAD responses are missing the content-length header
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
Also, the Content-Type
is missing in the HEAD response
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<*> {
...
}
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.
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.
I think this problem has been solved by this PR https://github.com/micronaut-projects/micronaut-core/pull/6903
@pditommaso unfortunately not
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.