Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions src/Http/Wolverine.Http.Tests/using_form_parameters.cs
Original file line number Diff line number Diff line change
Expand Up @@ -536,4 +536,23 @@ public void trouble_shoot_form_matching()
var variable = chain.TryFindOrCreateFormValue(parameter);
variable!.Creator.ShouldBeOfType<ParsedArrayFormValue>();
}

[Fact]
public void form_endpoints_honor_consumes_metadata_for_supported_request_formats()
{
// fillRequestType only seeds SupportedRequestFormats from
// IAcceptsMetadata when the endpoint has a body request type, is
// not a form endpoint, and is not a GET. Form endpoints used to
// fall through and rely on ASP.NET Core OpenAPI's
// "application/x-www-form-urlencoded" default, which silently
// dropped [Consumes("multipart/form-data")] and caused generated
// clients (Orval, NSwag) to emit URLSearchParams bodies instead of
// multipart for file-upload endpoints.
var chain = HttpChains.ChainFor("POST", "/form/multipart-consumes");
var apiDescription = chain!.CreateApiDescription("POST");

apiDescription.SupportedRequestFormats
.Select(f => f.MediaType)
.ShouldContain("multipart/form-data");
}
}
41 changes: 31 additions & 10 deletions src/Http/Wolverine.Http/HttpChain.ApiDescription.cs
Original file line number Diff line number Diff line change
Expand Up @@ -158,9 +158,39 @@ public ApiDescription CreateApiDescription(string httpMethod)
apiDescription.ParameterDescriptions.Add(parameterDescription);
}

// fillRequestType only honors [Consumes] / IAcceptsMetadata when the
// endpoint has a body request type, is not a form endpoint, and is
// not a GET (HasRequestType && !IsFormData && HttpMethod != "GET").
// Without this, form endpoints fall through and ASP.NET Core OpenAPI's
// GetFormRequestBody defaults SupportedRequestFormats to
// "application/x-www-form-urlencoded" — silently dropping
// [Consumes("multipart/form-data")] on file-upload endpoints and
// causing client generators (Orval, NSwag, Kiota) to emit
// URLSearchParams bodies instead of multipart.
if (apiDescription.SupportedRequestFormats.Count == 0 &&
apiDescription.ParameterDescriptions.Any(p =>
p.Source == BindingSource.Form || p.Source == BindingSource.FormFile))
{
copyAcceptsMetadataToRequestFormats(apiDescription);
}

return apiDescription;
}

private void copyAcceptsMetadataToRequestFormats(ApiDescription apiDescription)
{
foreach (var metadata in Endpoint!.Metadata.OfType<IAcceptsMetadata>())
{
foreach (var contentType in metadata.ContentTypes)
{
apiDescription.SupportedRequestFormats.Add(new ApiRequestFormat
{
MediaType = contentType
});
}
}
}

public override MiddlewareScoping Scoping => MiddlewareScoping.HttpEndpoints;

public override void UseForResponse(MethodCall methodCall)
Expand Down Expand Up @@ -333,16 +363,7 @@ private void fillRequestType(ApiDescription apiDescription)

apiDescription.ParameterDescriptions.Add(parameterDescription);

foreach (var metadata in Endpoint!.Metadata.OfType<IAcceptsMetadata>())
{
foreach (var contentType in metadata.ContentTypes)
{
apiDescription.SupportedRequestFormats.Add(new ApiRequestFormat
{
MediaType = contentType
});
}
}
copyAcceptsMetadataToRequestFormats(apiDescription);
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/Http/WolverineWebApi/Forms/FormEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ public static string PostWithFiles([FromForm] FormWithFiles form)
{
return $"{form.Name}|{form.Files?.Count}";
}

[WolverinePost("/form/multipart-consumes")]
[Consumes("multipart/form-data")]
public static string MultipartConsumes([FromForm] string value) => value ?? "";
}

public class FormWithFile
Expand Down
Loading