From 10d892882f62aa6df99d7148d6e223dafeb428f1 Mon Sep 17 00:00:00 2001 From: James Kleeh Date: Fri, 24 May 2019 08:19:46 -0400 Subject: [PATCH] Add a HEAD route for every GET (#1693) * Create a HEAD route for each GET. Closes #1489 * Fix test * Update AnnotatedFunctionRouteBuilder.java * Fix non deterministic test --- .../web/AnnotatedFunctionRouteBuilder.java | 35 +- .../function/web/WebFunctionSpec.groovy | 16 + .../micronaut/http/client/HttpGetSpec.groovy | 9 +- .../micronaut/http/client/HttpHeadSpec.groovy | 575 ++++++++++++++++++ .../server/netty/RoutingInBoundHandler.java | 11 +- .../http/server/netty/errors/ErrorSpec.groovy | 2 +- .../processors/ReadEndpointRouteBuilder.java | 6 +- .../endpoint/SimpleEndpointSpec.groovy | 18 + .../router/AnnotatedMethodRouteBuilder.java | 8 + 9 files changed, 657 insertions(+), 23 deletions(-) create mode 100644 http-client/src/test/groovy/io/micronaut/http/client/HttpHeadSpec.groovy diff --git a/function-web/src/main/java/io/micronaut/function/web/AnnotatedFunctionRouteBuilder.java b/function-web/src/main/java/io/micronaut/function/web/AnnotatedFunctionRouteBuilder.java index 380f8ca2790..812b08e34e6 100644 --- a/function-web/src/main/java/io/micronaut/function/web/AnnotatedFunctionRouteBuilder.java +++ b/function-web/src/main/java/io/micronaut/function/web/AnnotatedFunctionRouteBuilder.java @@ -94,7 +94,7 @@ public void process(BeanDefinition beanDefinition, ExecutableMethod met String functionName = beanDefinition.getValue(FunctionBean.class, String.class).orElse(methodName); String functionMethod = beanDefinition.getValue(FunctionBean.class, "method", String.class).orElse(null); - UriRoute route = null; + List routes = new ArrayList<>(2); MediaType[] consumes = method.getValue(Consumes.class, String[].class).map((types) -> Arrays.stream(types).map(MediaType::new).toArray(MediaType[]::new) ).orElse(null); @@ -109,7 +109,8 @@ public void process(BeanDefinition beanDefinition, ExecutableMethod met String argumentName = argumentNames[0]; int argCount = argumentNames.length; - route = POST(functionPath, beanDefinition, method); + UriRoute route = POST(functionPath, beanDefinition, method); + routes.add(route); if (argCount == 1) { route.body(argumentName); } @@ -141,7 +142,8 @@ public void process(BeanDefinition beanDefinition, ExecutableMethod met } } else if (Supplier.class.isAssignableFrom(declaringType) && methodName.equals("get")) { String functionPath = resolveFunctionPath(methodName, declaringType, functionName); - route = GET(functionPath, beanDefinition, method); + routes.add(GET(functionPath, beanDefinition, method)); + routes.add(HEAD(functionPath, beanDefinition, method)); } else { if (StringUtils.isNotEmpty(functionMethod)) { if (functionMethod.equals(methodName)) { @@ -150,10 +152,11 @@ public void process(BeanDefinition beanDefinition, ExecutableMethod met if (argCount < 3) { String functionPath = resolveFunctionPath(methodName, declaringType, functionName); if (argCount == 0) { - route = GET(functionPath, beanDefinition, method); - + routes.add(GET(functionPath, beanDefinition, method)); + routes.add(HEAD(functionPath, beanDefinition, method)); } else { - route = POST(functionPath, beanDefinition, method); + UriRoute route = POST(functionPath, beanDefinition, method); + routes.add(route); if (argCount == 2 || !ClassUtils.isJavaLangType(argumentTypes[0].getType())) { if (consumes == null) { consumes = new MediaType[] {MediaType.APPLICATION_JSON_TYPE}; @@ -168,17 +171,19 @@ public void process(BeanDefinition beanDefinition, ExecutableMethod met } } - if (route != null) { - if (LOG.isDebugEnabled()) { - LOG.debug("Created Route to Function: {}", route); - } + if (!routes.isEmpty()) { + for (UriRoute route: routes) { + if (LOG.isDebugEnabled()) { + LOG.debug("Created Route to Function: {}", route); + } - if (consumes != null) { - route.consumes(consumes); - } + if (consumes != null) { + route.consumes(consumes); + } - if (produces != null) { - route.produces(produces); + if (produces != null) { + route.produces(produces); + } } ClassLoadingReporter.reportBeanPresent(method.getReturnType().getType()); diff --git a/function-web/src/test/groovy/io/micronaut/function/web/WebFunctionSpec.groovy b/function-web/src/test/groovy/io/micronaut/function/web/WebFunctionSpec.groovy index 8e2eae4dea4..d3a0de0796b 100644 --- a/function-web/src/test/groovy/io/micronaut/function/web/WebFunctionSpec.groovy +++ b/function-web/src/test/groovy/io/micronaut/function/web/WebFunctionSpec.groovy @@ -66,6 +66,22 @@ class WebFunctionSpec extends Specification { embeddedServer.stop() } + void "test string supplier with HEAD"() { + given: + EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer) + RxHttpClient client = embeddedServer.applicationContext.createBean(RxHttpClient, embeddedServer.getURL()) + + when: + HttpResponse response = client.toBlocking().exchange(HttpRequest.HEAD('/supplier/string'), String) + + then: + response.code() == HttpStatus.OK.code + response.body() == null + + cleanup: + embeddedServer.stop() + } + void "test pojo supplier"() { given: EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer) diff --git a/http-client/src/test/groovy/io/micronaut/http/client/HttpGetSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/HttpGetSpec.groovy index a2297d62830..58993a635a2 100644 --- a/http-client/src/test/groovy/io/micronaut/http/client/HttpGetSpec.groovy +++ b/http-client/src/test/groovy/io/micronaut/http/client/HttpGetSpec.groovy @@ -43,6 +43,7 @@ import java.time.LocalDate * @since 1.0 */ class HttpGetSpec extends Specification { + @Shared @AutoCleanup EmbeddedServer embeddedServer = ApplicationContext.run(EmbeddedServer) @@ -95,7 +96,7 @@ class HttpGetSpec extends Specification { when: def flowable = Flowable.fromPublisher(client.exchange( - HttpRequest.GET("/get/error") + HttpRequest.GET("/get/error"), Argument.of(String), Argument.of(String) )) flowable.blockingFirst() @@ -104,27 +105,27 @@ class HttpGetSpec extends Specification { def e = thrown(HttpClientResponseException) e.message == "Server error" e.status == HttpStatus.INTERNAL_SERVER_ERROR + e.response.getBody(String).get() == "Server error" cleanup: client.stop() client.close() } - void "test 500 request with json body"() { given: HttpClient client = HttpClient.create(embeddedServer.getURL()) when: def flowable = Flowable.fromPublisher(client.exchange( - HttpRequest.GET("/get/jsonError") + HttpRequest.GET("/get/jsonError"), Argument.of(String), Argument.of(Map) )) flowable.blockingFirst() then: def e = thrown(HttpClientResponseException) - e.message == "Internal Server Error" + e.message == "{foo=bar}" e.status == HttpStatus.INTERNAL_SERVER_ERROR cleanup: diff --git a/http-client/src/test/groovy/io/micronaut/http/client/HttpHeadSpec.groovy b/http-client/src/test/groovy/io/micronaut/http/client/HttpHeadSpec.groovy new file mode 100644 index 00000000000..ff323929c10 --- /dev/null +++ b/http-client/src/test/groovy/io/micronaut/http/client/HttpHeadSpec.groovy @@ -0,0 +1,575 @@ +/* + * Copyright 2017-2019 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.http.client + +import io.micronaut.context.ApplicationContext +import io.micronaut.core.convert.format.Format +import io.micronaut.core.io.buffer.ByteBuffer +import io.micronaut.core.type.Argument +import io.micronaut.http.HttpRequest +import io.micronaut.http.HttpResponse +import io.micronaut.http.HttpStatus +import io.micronaut.http.MediaType +import io.micronaut.http.annotation.Controller +import io.micronaut.http.annotation.Get +import io.micronaut.http.annotation.Head +import io.micronaut.http.annotation.Header +import io.micronaut.http.annotation.QueryValue +import io.micronaut.http.client.annotation.Client +import io.micronaut.http.client.exceptions.HttpClientResponseException +import io.micronaut.runtime.server.EmbeddedServer +import io.reactivex.Flowable +import io.reactivex.functions.Consumer +import spock.lang.AutoCleanup +import spock.lang.Shared +import spock.lang.Specification +import spock.util.concurrent.PollingConditions + +import java.time.LocalDate + +/** + * @author Graeme Rocher + * @since 1.0 + */ +class HttpHeadSpec extends Specification { + + @Shared @AutoCleanup EmbeddedServer embeddedServer = + ApplicationContext.run(EmbeddedServer) + + void "test simple head request"() { + given: + HttpClient client = HttpClient.create(embeddedServer.getURL()) + + when: + def flowable = Flowable.fromPublisher(client.exchange( + HttpRequest.HEAD("/head/simple").header("Accept-Encoding", "gzip") + )) + Optional body = flowable.map({res -> + res.getBody(String)} + ).blockingFirst() + + then: + !body.isPresent() + + cleanup: + client.stop() + client.close() + } + + + void "test simple 404 request"() { + given: + HttpClient client = HttpClient.create(embeddedServer.getURL()) + + when: + def flowable = Flowable.fromPublisher(client.exchange( + HttpRequest.HEAD("/head/doesntexist") + )) + + flowable.blockingFirst() + + then: + def e = thrown(HttpClientResponseException) + e.message == "Not Found" + e.status == HttpStatus.NOT_FOUND + + cleanup: + client.stop() + client.close() + } + + void "test 500 request with body"() { + given: + HttpClient client = HttpClient.create(embeddedServer.getURL()) + + when: + def flowable = Flowable.fromPublisher(client.exchange( + HttpRequest.HEAD("/head/error"), Argument.of(String), Argument.of(String) + )) + + flowable.blockingFirst() + + then: + def e = thrown(HttpClientResponseException) + e.message == "Internal Server Error" + e.status == HttpStatus.INTERNAL_SERVER_ERROR + !e.response.getBody(String).isPresent() + + cleanup: + client.stop() + client.close() + } + + void "test 500 request with json body"() { + given: + HttpClient client = HttpClient.create(embeddedServer.getURL()) + + when: + def flowable = Flowable.fromPublisher(client.exchange( + HttpRequest.HEAD("/get/jsonError"), Argument.of(String), Argument.of(Map) + )) + + flowable.blockingFirst() + + then: + def e = thrown(HttpClientResponseException) + e.message == "Internal Server Error" + e.status == HttpStatus.INTERNAL_SERVER_ERROR + + cleanup: + client.stop() + client.close() + } + + void "test simple 404 request as VndError"() { + given: + HttpClient client = HttpClient.create(embeddedServer.getURL()) + + when: + def flowable = Flowable.fromPublisher(client.exchange( + HttpRequest.GET("/head/doesntexist") + )) + + def response = flowable.onErrorReturn({ error -> + if (error instanceof HttpClientResponseException) { + return HttpResponse.status(error.status).body(error.response.getBody(Map).orElse(null)) + } + throw error + }).blockingFirst() + + def body = response.body + + then: + body.isPresent() + body.get().message == "Page Not Found" + + cleanup: + client.stop() + client.close() + } + + void "test simple blocking get request"() { + + given: + def asyncClient = HttpClient.create(embeddedServer.getURL()) + BlockingHttpClient client = asyncClient.toBlocking() + + when: + HttpResponse response = client.exchange( + HttpRequest.HEAD("/head/simple"), + String + ) + + def body = response.getBody() + + then: + !body.isPresent() + + cleanup: + asyncClient.stop() + asyncClient.close() + } + + void "test simple get request with type"() { + given: + HttpClient client = new DefaultHttpClient(embeddedServer.getURL()) + + when: + Flowable> flowable = Flowable.fromPublisher(client.exchange( + HttpRequest.HEAD("/head/simple"), String + )) + HttpResponse response = flowable.blockingFirst() + def body = response.getBody() + + then: + response.status == HttpStatus.OK + !body.isPresent() + + cleanup: + client.stop() + } + + void "test simple exchange request with POJO"() { + given: + def context = ApplicationContext.run() + HttpClient client = context.createBean(HttpClient, embeddedServer.getURL()) + + when: + Flowable> flowable = Flowable.fromPublisher(client.exchange( + HttpRequest.HEAD("/head/pojo"), Book + )) + + HttpResponse response = flowable.blockingFirst() + Optional body = response.getBody() + + then: + !response.contentType.isPresent() + response.contentLength == -1 + response.status == HttpStatus.OK + !body.isPresent() + + cleanup: + client.stop() + } + + void "test simple retrieve request with POJO"() { + given: + def context = ApplicationContext.run() + HttpClient client = context.createBean(HttpClient, embeddedServer.getURL()) + + when: + Flowable flowable = Flowable.fromPublisher(client.retrieve( + HttpRequest.HEAD("/head/pojo"), Book + )).blockingFirst() + + then: + def ex = thrown(HttpClientResponseException) + ex.message == "Empty body" + + + cleanup: + client.stop() + } + + void "test simple get request with POJO list"() { + given: + def context = ApplicationContext.run() + HttpClient client = context.createBean(HttpClient, embeddedServer.getURL()) + + when: + Flowable>> flowable = Flowable.fromPublisher(client.exchange( + HttpRequest.HEAD("/head/pojoList"), Argument.of(List, Book) + )) + + HttpResponse> response = flowable.blockingFirst() + Optional> body = response.getBody() + + then: + !response.contentType.isPresent() + response.contentLength == -1 + response.status == HttpStatus.OK + !body.isPresent() + + cleanup: + client.stop() + } + + void "test get with @Client"() { + given: + MyGetHelper helper = embeddedServer.applicationContext.getBean(MyGetHelper) + + expect: + helper.simple() == null + helper.simpleSlash() == null + helper.simplePreceedingSlash() == null + helper.simpleDoubleSlash() == null + helper.queryParam() == null + } + + void "test query parameter with @Client interface"() { + given: + MyGetClient client = embeddedServer.applicationContext.getBean(MyGetClient) + + when: + client.queryParam('{"service":["test"]}') + + then: + def ex = thrown(HttpClientResponseException) + ex.message == "Empty body" + + when: + client.queryParam('foo', 'bar') + + then: + ex = thrown(HttpClientResponseException) + ex.message == "Empty body" + + when: + client.queryParam('foo%', 'bar') + + then: + ex = thrown(HttpClientResponseException) + ex.message == "Empty body" + } + + void "test body availability"() { + given: + RxHttpClient client = RxHttpClient.create(embeddedServer.getURL()) + + when: + Flowable flowable = client.exchange( + HttpRequest.HEAD("/head/simple") + ) + String body + flowable.firstOrError().subscribe((Consumer){ HttpResponse res -> + Thread.sleep(3000) + body = res.getBody(String).orElse(null) + }) + def conditions = new PollingConditions(timeout: 4) + + then: + conditions.eventually { + body == null + } + + cleanup: + client.stop() + } + + + void "test that Optional.empty() should return 404"() { + given: + HttpClient client = HttpClient.create(embeddedServer.getURL()) + + when: + def flowable = Flowable.fromPublisher(client.exchange( + HttpRequest.HEAD("/head/empty") + )) + + HttpResponse> response = flowable.blockingFirst() + + then: + def e = thrown(HttpClientResponseException) + e.message == "Not Found" + e.status == HttpStatus.NOT_FOUND + + cleanup: + client.stop() + client.close() + } + + void "test a non empty optional should return the value"() { + given: + HttpClient client = HttpClient.create(embeddedServer.getURL()) + + when: + String body = client.toBlocking().retrieve( + HttpRequest.HEAD("/head/notEmpty"), String + ) + + then: + def ex = thrown(HttpClientResponseException) + ex.message == "Empty body" + + cleanup: + client.stop() + client.close() + } + + void 'test format dates with @Format'() { + given: + MyGetClient client = embeddedServer.applicationContext.getBean(MyGetClient) + Date d = new Date(2018, 10, 20) + LocalDate dt = LocalDate.now() + + when: + client.formatDate(d) + + then: + def ex = thrown(HttpClientResponseException) + ex.message == "Empty body" + + when: + client.formatDateQuery(d) + + then: + ex = thrown(HttpClientResponseException) + ex.message == "Empty body" + + when: + client.formatDateTime(dt) + + then: + ex = thrown(HttpClientResponseException) + ex.message == "Empty body" + + when: + client.formatDateTimeQuery(dt) + + then: + ex = thrown(HttpClientResponseException) + ex.message == "Empty body" + } + + void "test a request with a custom host header"() { + given: + HttpClient client = HttpClient.create(embeddedServer.getURL()) + + when: + String body = client.toBlocking().retrieve( + HttpRequest.HEAD("/head/host").header("Host", "http://foo.com"), String + ) + + then: + def ex = thrown(HttpClientResponseException) + ex.message == "Empty body" + + cleanup: + client.stop() + client.close() + } + + @Controller("/head") + static class GetController { + + @Get(value = "/simple", produces = MediaType.TEXT_PLAIN) + String simple() { + return "success" + } + + @Get("/pojo") + Book pojo() { + return new Book(title: "The Stand") + } + + @Get("/pojoList") + List pojoList() { + return [ new Book(title: "The Stand") ] + } + + @Get(value = "/error", produces = MediaType.TEXT_PLAIN) + HttpResponse error() { + return HttpResponse.serverError().body("Server error") + } + + @Get("/jsonError") + HttpResponse jsonError() { + return HttpResponse.serverError().body([foo: "bar"]) + } + + @Get("/queryParam") + String queryParam(@QueryValue String foo) { + return foo + } + + @Get("/multipleQueryParam") + String queryParam(@QueryValue String foo, @QueryValue String bar) { + return foo + '-' + bar + } + + @Get("/empty") + Optional empty() { + return Optional.empty() + } + + @Get("/notEmpty") + Optional notEmpty() { + return Optional.of("not empty") + } + + @Get("/date/{myDate}") + String formatDate(@Format('yyyy-MM-dd') Date myDate) { + return myDate.toString() + } + + @Get("/dateTime/{myDate}") + String formatDateTime(@Format('yyyy-MM-dd') LocalDate myDate) { + return myDate.toString() + } + + @Get("/dateQuery") + String formatDateQuery(@QueryValue @Format('yyyy-MM-dd') Date myDate) { + return myDate.toString() + } + + @Get("/dateTimeQuery") + String formatDateTimeQuery(@QueryValue @Format('yyyy-MM-dd') LocalDate myDate) { + return myDate.toString() + } + + @Get("/host") + String hostHeader(@Header String host) { + return host + } + } + + static class Book { + String title + } + + static class Error { + String message + } + + @Client("/head") + static interface MyGetClient { + @Head(value = "/simple") + String simple() + + @Head("/pojo") + Book pojo() + + @Head("/pojoList") + List pojoList() + + @Head(value = "/error") + HttpResponse error() + + @Head("/jsonError") + HttpResponse jsonError() + + @Head("/queryParam") + String queryParam(@QueryValue String foo) + + @Head("/multipleQueryParam") + String queryParam(@QueryValue String foo, @QueryValue String bar) + + @Head("/date/{myDate}") + String formatDate(@Format('yyyy-MM-dd') Date myDate) + + @Head("/dateTime/{myDate}") + String formatDateTime(@Format('yyyy-MM-dd') LocalDate myDate) + + @Head("/dateQuery") + String formatDateQuery(@QueryValue @Format('yyyy-MM-dd') Date myDate) + + @Head("/dateTimeQuery") + String formatDateTimeQuery(@QueryValue @Format('yyyy-MM-dd') LocalDate myDate) + + } + + @javax.inject.Singleton + static class MyGetHelper { + private final RxStreamingHttpClient rxClientSlash + private final RxStreamingHttpClient rxClient + + MyGetHelper(@Client("/head/") RxStreamingHttpClient rxClientSlash, + @Client("/head") RxStreamingHttpClient rxClient) { + this.rxClient = rxClient + this.rxClientSlash = rxClientSlash + } + + String simple() { + rxClient.toBlocking().exchange(HttpRequest.HEAD("simple"), String).body() + } + + String simplePreceedingSlash() { + rxClient.toBlocking().exchange(HttpRequest.HEAD("/simple"), String).body() + } + + String simpleSlash() { + rxClientSlash.toBlocking().exchange(HttpRequest.HEAD("simple"), String).body() + } + + String simpleDoubleSlash() { + rxClientSlash.toBlocking().exchange(HttpRequest.HEAD("/simple"), String).body() + } + + String queryParam() { + rxClient.toBlocking().exchange(HttpRequest.HEAD("/queryParam?foo=a!b"), String).body() + } + } +} diff --git a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java index 49274866860..9e6fc19dc11 100644 --- a/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java +++ b/http-server-netty/src/main/java/io/micronaut/http/server/netty/RoutingInBoundHandler.java @@ -25,6 +25,7 @@ import io.micronaut.core.convert.ConversionService; import io.micronaut.core.io.Writable; import io.micronaut.core.io.buffer.ByteBuffer; +import io.micronaut.core.io.buffer.ReferenceCounted; import io.micronaut.core.reflect.ClassUtils; import io.micronaut.core.type.Argument; import io.micronaut.core.type.ReturnType; @@ -986,8 +987,14 @@ private RouteMatch prepareRouteForExecution(RouteMatch route, NettyHttpReq // here we transform the result of the controller action into a MutableHttpResponse Flowable> routePublisher = resultEmitter.map((message) -> { RouteMatch routeMatch = finalRoute; - HttpResponse response = messageToResponse(routeMatch, message); - MutableHttpResponse finalResponse = (MutableHttpResponse) response; + MutableHttpResponse finalResponse = messageToResponse(routeMatch, message); + if (requestReference.get().getMethod().equals(HttpMethod.HEAD)) { + finalResponse.getBody() + .filter(ReferenceCounted.class::isInstance) + .map(ReferenceCounted.class::cast) + .ifPresent(ReferenceCounted::release); + finalResponse.body(null); + } HttpStatus status = finalResponse.getStatus(); if (status.getCode() >= HttpStatus.BAD_REQUEST.getCode()) { diff --git a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/errors/ErrorSpec.groovy b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/errors/ErrorSpec.groovy index 00ad7da4a94..d5161d635dc 100644 --- a/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/errors/ErrorSpec.groovy +++ b/http-server-netty/src/test/groovy/io/micronaut/http/server/netty/errors/ErrorSpec.groovy @@ -80,7 +80,7 @@ class ErrorSpec extends AbstractMicronautSpec { def json = new JsonSlurper().parseText(response.getBody(String).orElse(null)) then: - json.message == 'Method [POST] not allowed. Allowed methods: [GET]' + json.message.matches('Method \\[POST\\] not allowed. Allowed methods: \\[(GET|HEAD), (GET|HEAD)\\]') json._links.self.href == '/errors/server-error' } diff --git a/management/src/main/java/io/micronaut/management/endpoint/processors/ReadEndpointRouteBuilder.java b/management/src/main/java/io/micronaut/management/endpoint/processors/ReadEndpointRouteBuilder.java index f5422fd313e..f5b6481a5c7 100644 --- a/management/src/main/java/io/micronaut/management/endpoint/processors/ReadEndpointRouteBuilder.java +++ b/management/src/main/java/io/micronaut/management/endpoint/processors/ReadEndpointRouteBuilder.java @@ -58,7 +58,11 @@ protected Class getSupportedAnnotation() { protected void registerRoute(ExecutableMethod method, String id) { Class declaringType = method.getDeclaringType(); UriTemplate template = buildUriTemplate(method, id); - final UriRoute uriRoute = GET(template.toString(), declaringType, method.getMethodName(), method.getArgumentTypes()); + UriRoute uriRoute = GET(template.toString(), declaringType, method.getMethodName(), method.getArgumentTypes()); + if (LOG.isDebugEnabled()) { + LOG.debug("Created Route to @Endpoint {}: {}", method.getDeclaringType().getName(), uriRoute); + } + uriRoute = HEAD(template.toString(), declaringType, method.getMethodName(), method.getArgumentTypes()); if (LOG.isDebugEnabled()) { LOG.debug("Created Route to @Endpoint {}: {}", method.getDeclaringType().getName(), uriRoute); } diff --git a/management/src/test/groovy/io/micronaut/management/endpoint/SimpleEndpointSpec.groovy b/management/src/test/groovy/io/micronaut/management/endpoint/SimpleEndpointSpec.groovy index f447a5f609d..8c8fc679312 100644 --- a/management/src/test/groovy/io/micronaut/management/endpoint/SimpleEndpointSpec.groovy +++ b/management/src/test/groovy/io/micronaut/management/endpoint/SimpleEndpointSpec.groovy @@ -54,6 +54,24 @@ class SimpleEndpointSpec extends Specification { server.close() } + void "test read simple endpoint with HEAD"() { + given: + EmbeddedServer server = ApplicationContext.run(EmbeddedServer, + ['endpoints.simple.myValue':'foo'], Environment.TEST) + RxHttpClient rxClient = server.applicationContext.createBean(RxHttpClient, server.getURL()) + + when: + def response = rxClient.exchange(HttpRequest.HEAD("/simple"), String).blockingFirst() + + then: + response.code() == HttpStatus.OK.code + response.body() == null + + cleanup: + rxClient.close() + server.close() + } + void "test read simple endpoint with argument"() { given: EmbeddedServer server = ApplicationContext.run(EmbeddedServer, diff --git a/router/src/main/java/io/micronaut/web/router/AnnotatedMethodRouteBuilder.java b/router/src/main/java/io/micronaut/web/router/AnnotatedMethodRouteBuilder.java index 8bfe8dedd9d..c8701da3ca1 100644 --- a/router/src/main/java/io/micronaut/web/router/AnnotatedMethodRouteBuilder.java +++ b/router/src/main/java/io/micronaut/web/router/AnnotatedMethodRouteBuilder.java @@ -65,6 +65,14 @@ public AnnotatedMethodRouteBuilder(ExecutionHandleLocator executionHandleLocator if (LOG.isDebugEnabled()) { LOG.debug("Created Route: {}", route); } + route = HEAD(resolveUri(bean, uri, + method, + uriNamingStrategy), + bean, + method).produces(produces); + if (LOG.isDebugEnabled()) { + LOG.debug("Created Route: {}", route); + } }); httpMethodsHandlers.put(Post.class, (BeanDefinition bean, ExecutableMethod method) -> {