Inconsistent exceptions thrown when OpenAPI has badly formatted default values
Closed this issue · 0 comments
The following scenario outline describes the behaviour when an OpenAPI specification defines default values for parameters, but where those defaults are invalid:
The outline defines three examples. The first seems clear enough: a default of This is certainly not a date
has been supplied for a string
with format date
. The next is more subtle: a string
with format date-time
has been given a default value of 20170721T173228Z
which certainly looks like it might be a valid, but it turns out not to be. The set of acceptable formats is given at https://github.com/menes-dotnet/Menes/blob/main/Solutions/Menes.Abstractions/Menes/Validation/OpenApiSchemaValidator.cs#L313-L330 and of the formats that include both date and time, all require dashes between the date components and colons between the time components. So 2017-07-21T17:32:28
would be acceptable but 20170721T173228Z
is not.
The third is also not entirely obvious. It's an integer type and the value 9223372036854775808123123123
looks like an integer. However, the format is int64
, and that particular value is too large to fit into an int64
, so it is invalid.
The baffling part of this spec is that it requires different exception types for each example. The spec executes successfully, but it's not clear whether it should.
All three of these are instances of the same mistake: a particular kind of error in the OpenAPI specification. (In all three cases, the spec generates an OpenAPI spec that defines a parameter with a default value that is valid according to its type but invalid according to its format.)
I don't see a good reason why the correct exception should not be OpenApiSpecificationException
in all three cases. As far as I can tell, the specification just describes inconsistent behaviour that is an accident of the implementation details of how these three variations on the mistake happen to be detected.
The reason we see a FormatException
in the first case is that we happen to be relying on Newtonsoft.Json
to do the conversion. That's happening (in a not entirely obvious way) in this line of code:
If the text isn't in one of the formats that Newtonsoft.Json
recognizes for a DateTimeOffset
, that line throws a FormatException
. The first example in the scenario outline describes this fact correctly, but I'm unconvinced that this is desirable for that to be the exception that ultimately emerges. (The net effect will likely be a 500 server error, which is the correct externally visible behaviour as far as clients of the service are concerned—this scenario is indeed a server misconfiguration. But it's not a particularly helpful error for the owner of the service when diagnosing the problem.)
The second example looks actively wrong: an OpenApiBadRequestException
is thrown even if nothing is wrong with the request. The reason this happens is that the following sequence occurs:
- The test creates a request that does not include the parameter (which is fine because the parameter is optional—the whole point here is that a default value has been specified)
- The
HttpRequestParameterBuilder
tries to discover the default for this parameter: - Since the parameter type is string, the switch expression at the top of that method calls
this.ConvertValue
which correctly finds the MenesStringConverter
, and calls itsConvertFrom
method here: - The
StringConverter
callsvalidator.ValidateAndThrow
: - The validator detects that the format is not an acceptable
date-time
here: - The
ValidateAndThrow
method reports this validation failure by throwing anOpenApiBadRequestException
here:
The basic problem seems to be that OpenApiSchemaValidator.ValidateAndThrow
presumes it is being asked to validate data that came from a request (a presumption expressed in the fact that it throws an OpenApiBadRequestException
) but there are scenarios in which this method is being asked to validate data that did not come from a request.
The reason the third example in this scenario outline does actually produce what seems like the correct exception is that the problem here is detected in an entirely different way, but possibly by accident. It turns out that if you supply Microsoft.OpenApi
with a default value that is an integer numeric value that's perfectly valid but too large to fit in a 64-bit int (e.g. "default": 9223372036854775808123123123
) it reports it as an OpenApiString
! (The OpenApiSchema
object will still correctly report that the type was an int64
.)
This is detected by this code:
This appears to be looking for type mismatches, e.g., mistakes of this kind:
{ "type": "integer", "format": "int64", "default": "foo" }
It so happens that it also picks up the case where the numeric value is too large:
{ "type": "integer", "format": "int64", "default": 9223372036854775808123123123 }
This works only because Microsoft.OpenApi
reported something that was a number as though it had been a string. The effect is that this code misdiagnoses this as a type mismatch but then goes on to throw the right kind of exception anyway. Perhaps this was deliberate (although it's such a non-obvious way of achieving this that a comment wouldn't have gone amiss).
In conclusion, the three examples here get caught by three completely different code paths in our current implementation. These happen to use different exception types to report the problems, but it looks like the test just reflects this implementation accident, rather than expressing what we would actually want.
(A side issue is that the test here produces Open API schemas that aren't actually valid JSON. When the DefaultValue is a string, it doesn't wrap it in quotes, so you end up with something like { "default": This is certainly not a date }
. It turns out that Microsoft.OpenApi
tolerates this, and reports it as though you'd written { "default": "This is certainly not a date" }
.)