Skip to content

Commit 32e9deb

Browse files
authored
4.x: Fixed missing required query and header parameters in declarative (helidon-io#10668)
* Missing header or query parameter that is required in declarative method will now end with a bad request, rather than internal server error.
1 parent 814c98b commit 32e9deb

10 files changed

Lines changed: 133 additions & 17 deletions

File tree

declarative/codegen/src/main/java/io/helidon/declarative/codegen/http/HttpTypes.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,10 @@ public final class HttpTypes {
8686
* HTTP media type.
8787
*/
8888
public static final TypeName HTTP_MEDIA_TYPE = TypeName.create("io.helidon.http.HttpMediaType");
89+
/**
90+
* Bad request exception.
91+
*/
92+
public static final TypeName BAD_REQUEST_EXCEPTION = TypeName.create("io.helidon.http.BadRequestException");
8993

9094
private HttpTypes() {
9195
}

declarative/codegen/src/main/java/io/helidon/declarative/codegen/http/webserver/AbstractParametersProvider.java

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import io.helidon.common.types.TypeNames;
2525

2626
import static io.helidon.declarative.codegen.DeclarativeTypes.COMMON_MAPPERS;
27+
import static io.helidon.declarative.codegen.http.HttpTypes.BAD_REQUEST_EXCEPTION;
2728

2829
abstract class AbstractParametersProvider {
2930
void codegenFromParameters(ContentBuilder<?> contentBuilder, TypeName parameterType, String paramName, boolean optional) {
@@ -55,12 +56,27 @@ void codegenFromParameters(ContentBuilder<?> contentBuilder, TypeName parameterT
5556
contentBuilder
5657
.addContent(".first(\"")
5758
.addContent(paramName)
58-
.addContent("\").");
59-
getMethod(contentBuilder, parameterType);
59+
.addContentLine("\")")
60+
.increaseContentPadding()
61+
.increaseContentPadding()
62+
.addContent(".");
63+
asMethod(contentBuilder, parameterType);
64+
// add .orElseThrow() in case the parameter is missing
65+
contentBuilder.addContentLine("")
66+
.addContent(".orElseThrow(() -> new ")
67+
.addContent(BAD_REQUEST_EXCEPTION)
68+
.addContent("(\"")
69+
.addContent(providerType())
70+
.addContent(" ")
71+
.addContent(paramName)
72+
.addContentLine(" is not present in the request.\"));")
73+
.decreaseContentPadding()
74+
.decreaseContentPadding();
6075
}
61-
6276
}
6377

78+
abstract String providerType();
79+
6480
void asMethod(ContentBuilder<?> content, TypeName type) {
6581
TypeName boxed = type.boxed();
6682

declarative/codegen/src/main/java/io/helidon/declarative/codegen/http/webserver/ParamProviderHttpEntity.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,9 @@ public boolean codegen(ParameterCodegenContext ctx) {
4242

4343
return true;
4444
}
45+
46+
@Override
47+
String providerType() {
48+
return "Entity";
49+
}
4550
}

declarative/codegen/src/main/java/io/helidon/declarative/codegen/http/webserver/ParamProviderHttpHeader.java

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import io.helidon.service.codegen.DefaultsParams;
2929
import io.helidon.service.codegen.FieldHandler;
3030

31+
import static io.helidon.declarative.codegen.http.HttpTypes.BAD_REQUEST_EXCEPTION;
3132
import static io.helidon.declarative.codegen.http.HttpTypes.HTTP_HEADER_PARAM_ANNOTATION;
3233

3334
class ParamProviderHttpHeader extends AbstractParametersProvider implements HttpParameterCodegenProvider {
@@ -62,17 +63,24 @@ public boolean codegen(ParameterCodegenContext ctx) {
6263
ContentBuilder<?> contentBuilder = ctx.contentBuilder();
6364
String serverRequestParamName = ctx.serverRequestParamName();
6465

66+
// we always want `req.headers().find(HEADER_CONSTANT).map(it -> it.get(SomeType.class)`
6567
contentBuilder.addContent(serverRequestParamName)
66-
.addContent(".headers()");
68+
.addContent(".headers()")
69+
.addContent(".find(")
70+
.addContent(headerConstantName)
71+
.addContentLine(")")
72+
.increaseContentPadding()
73+
.increaseContentPadding()
74+
.addContent(".map(it -> it.");
75+
getMethod(contentBuilder, realType);
76+
contentBuilder.addContent(")");
6777

6878
// add generated code to obtain the header from request
6979
if (parameterType.isOptional() || defaultCode.isPresent()) {
70-
contentBuilder.addContent(".find(")
71-
.addContent(headerConstantName)
72-
.addContentLine(")")
73-
.addContent(".map(it -> it.");
74-
getMethod(contentBuilder, realType);
75-
contentBuilder.addContentLine(")");
80+
// optional is what we expect
81+
contentBuilder.addContentLine(";")
82+
.decreaseContentPadding()
83+
.decreaseContentPadding();
7684

7785
if (defaultCode.isPresent()) {
7886
DefaultsCodegen.DefaultCode defaultInfo = defaultCode.get();
@@ -93,13 +101,24 @@ public boolean codegen(ParameterCodegenContext ctx) {
93101
contentBuilder.addContentLine(";");
94102
}
95103
} else {
96-
contentBuilder.addContent(".get(")
104+
// add .orElseThrow() in case the header is missing
105+
contentBuilder.addContentLine("")
106+
.addContent(".orElseThrow(() -> new ")
107+
.addContent(BAD_REQUEST_EXCEPTION)
108+
.addContent("(\"Header \" + ")
97109
.addContent(headerConstantName)
98-
.addContent(").");
99-
getMethod(contentBuilder, parameterType);
100-
contentBuilder.addContentLine(";");
110+
.addContent(".defaultCase() + ")
111+
.addContentLiteral(" is not present in the request.")
112+
.addContentLine("));")
113+
.decreaseContentPadding()
114+
.decreaseContentPadding();
101115
}
102116

103117
return true;
104118
}
119+
120+
@Override
121+
String providerType() {
122+
return "Header";
123+
}
105124
}

declarative/codegen/src/main/java/io/helidon/declarative/codegen/http/webserver/ParamProviderHttpPathParam.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,9 @@ public boolean codegen(ParameterCodegenContext ctx) {
4949

5050
return true;
5151
}
52+
53+
@Override
54+
String providerType() {
55+
return "Path parameter";
56+
}
5257
}

declarative/codegen/src/main/java/io/helidon/declarative/codegen/http/webserver/ParamProviderHttpQuery.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,4 +77,8 @@ public boolean codegen(ParameterCodegenContext ctx) {
7777
return true;
7878
}
7979

80+
@Override
81+
String providerType() {
82+
return "Query parameter";
83+
}
8084
}

declarative/tests/codegen/src/test/java/io/helidon/declarative/codegen/http/DeclarativeCodegenHttpTypesTest.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.Set;
2525

2626
import io.helidon.common.types.TypeName;
27+
import io.helidon.http.BadRequestException;
2728
import io.helidon.http.Header;
2829
import io.helidon.http.HeaderName;
2930
import io.helidon.http.HeaderNames;
@@ -83,6 +84,7 @@ void testTypes() {
8384
checkField(toCheck, checked, fields, "HTTP_ENTITY_ANNOTATION", Http.Entity.class);
8485
checkField(toCheck, checked, fields, "HTTP_HEADER_FUNCTION", Http.HeaderFunction.class);
8586
checkField(toCheck, checked, fields, "HTTP_MEDIA_TYPE", HttpMediaType.class);
87+
checkField(toCheck, checked, fields, "BAD_REQUEST_EXCEPTION", BadRequestException.class);
8688

8789
assertThat("If the collection is not empty, please add appropriate checkField line to this test",
8890
toCheck,

declarative/tests/http/src/main/java/io/helidon/declarative/tests/http/GreetServiceEndpoint.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,18 @@ class GreetServiceEndpoint implements GreetService {
7373
this.greeting.set(greeting);
7474
}
7575

76+
@Http.GET
77+
@Http.Path("/custom-header")
78+
String customHeaderParam(@Http.HeaderParam("X-CUSTOM") String header) {
79+
return header;
80+
}
81+
82+
@Http.GET
83+
@Http.Path("/query-param")
84+
String queryParam(@Http.QueryParam("param") String queryParam) {
85+
return queryParam;
86+
}
87+
7688
/**
7789
* Return a worldly greeting message.
7890
*/

declarative/tests/http/src/main/resources/application.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ app:
2020
server:
2121
port: 8080
2222
host: 0.0.0.0
23+
error-handling:
24+
# THIS MUST NOT BE USED IN PRODUCTION!
25+
# exception message will be sent back over the network - this may give potential attackers information about your application
26+
include-entity: true
2327

2428
greet-service.client:
2529
uri: "http://localhost:${test.server.port}"

declarative/tests/http/src/test/java/io/helidon/declarative/tests/http/DeclarativeHttpTest.java

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
import java.util.Map;
2121
import java.util.Optional;
2222

23+
import io.helidon.http.HeaderName;
24+
import io.helidon.http.HeaderNames;
25+
import io.helidon.http.HeaderValues;
2326
import io.helidon.http.HttpException;
2427
import io.helidon.http.Status;
2528
import io.helidon.service.registry.Lookup;
@@ -105,12 +108,54 @@ void testErrorHandler() {
105108
hasItems(GreetServiceEndpoint.class.getName() + ".updateGreetingHandler(jakarta.json.JsonObject)"));
106109
}
107110

111+
@Test
112+
void testCustomHeaderParamSuccess() {
113+
HeaderName customHeader = HeaderNames.create("X-CUSTOM");
114+
115+
var response = client.get("/greet/custom-header")
116+
.header(HeaderValues.create(customHeader, "Expected-value"))
117+
.request(String.class);
118+
119+
assertThat(response.status(), is(Status.OK_200));
120+
assertThat(response.entity(), is("Expected-value"));
121+
}
122+
123+
@Test
124+
void testCustomHeaderParamFailure() {
125+
var response = client.get("/greet/custom-header")
126+
.request(String.class);
127+
128+
assertThat(response.status(), is(Status.BAD_REQUEST_400));
129+
// this requires configuration option server.error-handling.include-entity set to true
130+
assertThat(response.entity(), is("Header X-CUSTOM is not present in the request."));
131+
}
132+
133+
@Test
134+
void testQueryParamSuccess() {
135+
var response = client.get("/greet/query-param")
136+
.queryParam("param", "Expected-value")
137+
.request(String.class);
138+
139+
assertThat(response.status(), is(Status.OK_200));
140+
assertThat(response.entity(), is("Expected-value"));
141+
}
142+
143+
@Test
144+
void testQueryParamFailure() {
145+
var response = client.get("/greet/query-param")
146+
.request(String.class);
147+
148+
assertThat(response.status(), is(Status.BAD_REQUEST_400));
149+
// this requires configuration option server.error-handling.include-entity set to true
150+
assertThat(response.entity(), is("Query parameter param is not present in the request."));
151+
}
152+
108153
@Test
109154
void testTypedClient() {
110155
GreetServiceClient typedClient = registry.get(Lookup.builder()
111-
.addContract(GreetServiceClient.class)
112-
.addQualifier(Qualifier.create(RestClient.Client.class))
113-
.build());
156+
.addContract(GreetServiceClient.class)
157+
.addQualifier(Qualifier.create(RestClient.Client.class))
158+
.build());
114159

115160
String message = typedClient.getDefaultMessageHandlerPlain();
116161
assertThat(message, is("Hello World!"));

0 commit comments

Comments
 (0)