Skip to content

Commit 797ccf1

Browse files
[OpenAPI] Improve missing query parameter error (#9492)
1 parent d731a0c commit 797ccf1

File tree

6 files changed

+62
-36
lines changed

6 files changed

+62
-36
lines changed

src/HotChocolate/Adapters/src/Adapters.OpenApi.Core/Execution/DynamicEndpointMiddleware.cs

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -333,13 +333,19 @@ private static bool TryGetValueForParameter(
333333
return false;
334334
}
335335

336+
if (leaf.IsNonNullType)
337+
{
338+
throw new BadRequestException(
339+
$"Required route parameter '{leaf.ParameterKey}' is missing");
340+
}
341+
336342
parameterValue = s_nullValueNode;
337343
return true;
338344
}
339345

340346
try
341347
{
342-
parameterValue = ParseValueNode(value, leaf.Type);
348+
parameterValue = ParseValueNode(value, leaf.NamedType);
343349
return true;
344350
}
345351
catch (InvalidFormatException)
@@ -350,20 +356,32 @@ private static bool TryGetValueForParameter(
350356

351357
if (leaf.ParameterType is OpenApiEndpointParameterType.Query)
352358
{
353-
if (!query.TryGetValue(leaf.ParameterKey, out var values) || values is not [{ } value])
359+
if (!query.TryGetValue(leaf.ParameterKey, out var values))
354360
{
355361
if (leaf.HasDefaultValue)
356362
{
357363
return false;
358364
}
359365

366+
if (leaf.IsNonNullType)
367+
{
368+
throw new BadRequestException(
369+
$"Required query parameter '{leaf.ParameterKey}' is missing");
370+
}
371+
360372
parameterValue = s_nullValueNode;
361373
return true;
362374
}
363375

376+
if (values is not [{ } value])
377+
{
378+
throw new BadRequestException(
379+
$"Query parameter '{leaf.ParameterKey}' can only be specified once.");
380+
}
381+
364382
try
365383
{
366-
parameterValue = ParseValueNode(value, leaf.Type);
384+
parameterValue = ParseValueNode(value, leaf.NamedType);
367385
return true;
368386
}
369387
catch (InvalidFormatException)

src/HotChocolate/Adapters/src/Adapters.OpenApi.Core/Execution/OpenApiEndpointDescriptor.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,10 @@ internal sealed class VariableValueInsertionTrie
2121

2222
internal sealed record VariableValueInsertionTrieLeaf(
2323
string ParameterKey,
24-
ITypeDefinition Type,
24+
ITypeDefinition NamedType,
2525
OpenApiEndpointParameterType ParameterType,
26-
bool HasDefaultValue) : IVariableValueInsertionTrieSegment;
26+
bool HasDefaultValue,
27+
bool IsNonNullType) : IVariableValueInsertionTrieSegment;
2728

2829
internal enum OpenApiEndpointParameterType
2930
{

src/HotChocolate/Adapters/src/Adapters.OpenApi.Core/Execution/OpenApiEndpointFactory.cs

Lines changed: 8 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ private static OpenApiEndpointDescriptor CreateEndpointDescriptor(
9696

9797
var responseNameToExtract = rootField.Alias?.Value ?? rootField.Name.Value;
9898

99-
var route = CreateRoutePattern(endpointDefinition.Route);
99+
var route = RoutePatternFactory.Parse(endpointDefinition.Route);
100100

101101
var parameterTrie = new VariableValueInsertionTrie();
102102

@@ -123,7 +123,7 @@ void InsertParametersIntoTrie(
123123
{
124124
foreach (var parameter in parameters)
125125
{
126-
var (inputType, hasDefaultValue) = GetParameterDetails(
126+
var (inputType, hasDefaultValue, isNonNullType) = GetParameterDetails(
127127
parameter,
128128
endpointDefinition.OperationDefinition,
129129
schema);
@@ -132,7 +132,8 @@ void InsertParametersIntoTrie(
132132
parameter.Key,
133133
inputType,
134134
parameterType,
135-
hasDefaultValue);
135+
hasDefaultValue,
136+
isNonNullType);
136137

137138
var inputObjectPath = parameter.InputObjectPath;
138139

@@ -185,7 +186,7 @@ void InsertParametersIntoTrie(
185186
}
186187
}
187188

188-
private static (ITypeDefinition Type, bool HasDefaultValue) GetParameterDetails(
189+
private static (ITypeDefinition Type, bool HasDefaultValue, bool IsNonNullType) GetParameterDetails(
189190
OpenApiEndpointDefinitionParameter parameter,
190191
OperationDefinitionNode operation,
191192
ISchemaDefinition schema)
@@ -195,6 +196,7 @@ private static (ITypeDefinition Type, bool HasDefaultValue) GetParameterDetails(
195196

196197
var currentType = schema.Types[variable.Type.NamedType().Name.Value];
197198
var hasDefaultValue = variable.DefaultValue is not null;
199+
var isNonNullType = variable.Type.IsNonNullType();
198200

199201
if (parameter.InputObjectPath is { Length: > 0 })
200202
{
@@ -209,35 +211,10 @@ private static (ITypeDefinition Type, bool HasDefaultValue) GetParameterDetails(
209211

210212
currentType = field.Type.NamedType();
211213
hasDefaultValue = field.DefaultValue is not null;
214+
isNonNullType = field.Type.IsNonNullType();
212215
}
213216
}
214217

215-
return (currentType, hasDefaultValue);
216-
}
217-
218-
private static RoutePattern CreateRoutePattern(string route)
219-
{
220-
return RoutePatternFactory.Parse(route);
221-
// var segments = new List<RoutePatternPathSegment>();
222-
//
223-
// foreach (var segment in route.Segments)
224-
// {
225-
// if (segment is OpenApiRouteSegmentLiteral stringSegment)
226-
// {
227-
// segments.Add(
228-
// RoutePatternFactory.Segment(
229-
// RoutePatternFactory.LiteralPart(stringSegment.Value)));
230-
// }
231-
// else if (segment is OpenApiRouteSegmentParameter mapSegment)
232-
// {
233-
// // We do not apply route constraints here, as they are not meant for validation but to disambiguate routes:
234-
// // https://learn.microsoft.com/en-us/aspnet/core/fundamentals/routing#route-constraints
235-
// segments.Add(
236-
// RoutePatternFactory.Segment(
237-
// RoutePatternFactory.ParameterPart(mapSegment.Key)));
238-
// }
239-
// }
240-
//
241-
// return RoutePatternFactory.Pattern(segments);
218+
return (currentType, hasDefaultValue, isNonNullType);
242219
}
243220
}

src/HotChocolate/Adapters/test/Adapters.OpenApi.Tests/Endpoints/HttpEndpointIntegrationTestBase.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,27 @@ public async Task Http_Get_With_Query_Parameter()
8484
response.MatchSnapshot();
8585
}
8686

87+
[Fact]
88+
public async Task Http_Get_With_Missing_Required_Query_Parameter()
89+
{
90+
// arrange
91+
var storage = new TestOpenApiDefinitionStorage(
92+
"""
93+
query SearchProducts($text: String, $first: Int!)
94+
@http(method: GET, route: "/products/search", queryParameters: ["text", "first"]) {
95+
searchProductsPaginated(text: $text, first: $first)
96+
}
97+
""");
98+
var server = CreateTestServer(storage);
99+
var client = server.CreateClient();
100+
101+
// act
102+
var response = await client.GetAsync("/products/search?text=Chair");
103+
104+
// assert
105+
response.MatchSnapshot();
106+
}
107+
87108
[Fact]
88109
public async Task Http_Get_Without_Query_Parameter_That_Has_Default_Value()
89110
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
Headers:
2+
Content-Type: application/problem+json
3+
-------------------------->
4+
Status Code: BadRequest
5+
-------------------------->
6+
{"type":"https://tools.ietf.org/html/rfc9110#section-15.5.1","title":"Bad Request","status":400,"detail":"Required query parameter 'first' is missing"}

src/HotChocolate/Adapters/test/Adapters.OpenApi.Tests/TestSchema.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ public string SearchProducts(string? text, float? minPrice)
9494
var formattedMinPrice = minPrice?.ToString(CultureInfo.InvariantCulture);
9595
return $"Searched for: {text ?? "all"}, minPrice: {formattedMinPrice}";
9696
}
97+
98+
public string SearchProductsPaginated(string? text, int first)
99+
=> $"Searched for: {text ?? "all"}, first: {first}";
97100
}
98101

99102
public class Mutation

0 commit comments

Comments
 (0)