diff --git a/src/HotChocolate/AspNetCore/src/Transport.Http/DefaultGraphQLHttpClient.cs b/src/HotChocolate/AspNetCore/src/Transport.Http/DefaultGraphQLHttpClient.cs index 088782085bf..e83c3630d77 100644 --- a/src/HotChocolate/AspNetCore/src/Transport.Http/DefaultGraphQLHttpClient.cs +++ b/src/HotChocolate/AspNetCore/src/Transport.Http/DefaultGraphQLHttpClient.cs @@ -291,7 +291,7 @@ private static HttpContent CreateMultipartContent( var fileContent = new StreamContent(file.OpenReadStream()); if (!string.IsNullOrEmpty(file.ContentType)) { - fileContent.Headers.ContentType = new MediaTypeHeaderValue(file.ContentType); + fileContent.Headers.TryAddWithoutValidation("Content-Type", file.ContentType); } form.Add(fileContent, i.ToString(), file.Name); @@ -394,7 +394,7 @@ private static HttpContent CreateMultipartContent( var fileContent = new StreamContent(fileInfo.File.OpenRead()); if (!string.IsNullOrEmpty(fileInfo.File.ContentType)) { - fileContent.Headers.ContentType = new MediaTypeHeaderValue(fileInfo.File.ContentType); + fileContent.Headers.TryAddWithoutValidation("Content-Type", fileInfo.File.ContentType); } form.Add(fileContent, fileInfo.Name, fileInfo.File.FileName); diff --git a/src/HotChocolate/AspNetCore/test/Transport.Http.Tests/GraphQLHttpClientTests.cs b/src/HotChocolate/AspNetCore/test/Transport.Http.Tests/GraphQLHttpClientTests.cs index b6705c7520f..4bbd76245d6 100644 --- a/src/HotChocolate/AspNetCore/test/Transport.Http.Tests/GraphQLHttpClientTests.cs +++ b/src/HotChocolate/AspNetCore/test/Transport.Http.Tests/GraphQLHttpClientTests.cs @@ -975,6 +975,7 @@ public async Task Get_Subscription_Over_SSE_With_Errors() [Theory] [InlineData((string?)null)] [InlineData("application/pdf")] + [InlineData("text/plain; charset=utf-8")] public async Task Post_GraphQL_FileUpload(string? contentType) { // arrange diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/FileUploadTests.cs b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/FileUploadTests.cs index 26005a7e78c..014bacc5f2c 100644 --- a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/FileUploadTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/FileUploadTests.cs @@ -58,6 +58,54 @@ public async Task Upload_Single_File() await MatchSnapshotAsync(gateway, operation, result, rawRequest: rawRequest); } + [Fact] + public async Task Upload_Single_File_Should_Forward_Parameterized_ContentType() + { + // arrange + using var server1 = CreateSourceSchema( + "A", + b => b.AddQueryType().AddUploadType()); + + using var gateway = await CreateCompositeSchemaAsync( + [ + ("A", server1) + ]); + + // act + using var client = GraphQLHttpClient.Create(gateway.CreateClient()); + + var stream = new MemoryStream("abc"u8.ToArray()); + + var operation = new OperationRequest( + """ + query ($file: Upload!) { + singleUpload(file: $file) { + fileName + contentType + content + } + } + """, + variables: new Dictionary + { + ["file"] = new FileReference(() => stream, "test.txt", "text/plain; charset=utf-8") + }); + + RawRequest? rawRequest = null; + var request = new GraphQLHttpRequest(operation, new Uri("http://localhost:5000/graphql")) + { + Method = GraphQLHttpMethod.Post, + EnableFileUploads = true, + OnMessageCreated = (_, request, _) => rawRequest = GetRawRequest(request) + }; + + // act + var result = await client.SendAsync(request, TestContext.Current.CancellationToken); + + // assert + await MatchSnapshotAsync(gateway, operation, result, rawRequest: rawRequest); + } + [Fact] public async Task Upload_Single_File_In_Input_Object() { diff --git a/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File_Should_Forward_Parameterized_ContentType.yaml b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File_Should_Forward_Parameterized_ContentType.yaml new file mode 100644 index 00000000000..1dc824bdbce --- /dev/null +++ b/src/HotChocolate/Fusion/test/Fusion.AspNetCore.Tests/__snapshots__/FileUploadTests.Upload_Single_File_Should_Forward_Parameterized_ContentType.yaml @@ -0,0 +1,146 @@ +title: Upload_Single_File_Should_Forward_Parameterized_ContentType +request: + contentType: multipart/form-data; boundary="f56524ab-5626-4955-b296-234a097b44f6" + body: | + --f56524ab-5626-4955-b296-234a097b44f6 + Content-Type: application/json; charset=utf-8 + Content-Disposition: form-data; name=operations + + {"query":"query ($file: Upload!) {\n singleUpload(file: $file) {\n fileName\n contentType\n content\n }\n}","variables":{"file":null}} + --f56524ab-5626-4955-b296-234a097b44f6 + Content-Type: application/json; charset=utf-8 + Content-Disposition: form-data; name=map + + {"0":["variables.file"]} + --f56524ab-5626-4955-b296-234a097b44f6 + Content-Type: text/plain; charset=utf-8 + Content-Disposition: form-data; name=0; filename=test.txt; filename*=utf-8''test.txt + + abc + --f56524ab-5626-4955-b296-234a097b44f6-- +response: + body: | + { + "data": { + "singleUpload": { + "fileName": "test.txt", + "contentType": "text/plain; charset=utf-8", + "content": "abc" + } + } + } +sourceSchemas: + - name: A + schema: | + schema { + query: Query + } + + type Query { + singleUpload(file: Upload!): FileUploadResult! + nullableUpload(file: Upload): FileUploadResult + singleUploadWithInput(input: FileInput!): FileUploadResult! + multiUpload(files: [Upload!]!): [FileUploadResult!]! + multiUploadWithInput(input: FilesInput!): [FileUploadResult!]! + } + + type FileUploadResult { + fileName: String! + contentType: String + content: String! + } + + input FileInput { + file: Upload! + } + + input FilesInput { + files: [Upload!]! + } + + "Defines the possible serialization types for GraphQL scalar values." + enum ScalarSerializationType { + "The scalar serializes to a string value." + STRING + "The scalar serializes to a boolean value." + BOOLEAN + "The scalar serializes to an integer value." + INT + "The scalar serializes to a floating-point value." + FLOAT + "The scalar serializes to an object value." + OBJECT + "The scalar serializes to a list value." + LIST + } + + "The `Upload` scalar type represents a file upload." + scalar Upload @serializeAs(type: STRING) + + directive @serializeAs( + "The primitive type a scalar is serialized to." + type: [ScalarSerializationType!]! + "The ECMA-262 regex pattern that the serialized scalar value conforms to." + pattern: String + ) on SCALAR + interactions: + - request: + accept: application/graphql-response+json; charset=utf-8, application/json; charset=utf-8, application/jsonl; charset=utf-8, text/event-stream; charset=utf-8 + contentType: multipart/form-data; boundary="f56524ab-5626-4955-b296-234a097b44f6" + body: | + --f56524ab-5626-4955-b296-234a097b44f6 + Content-Type: application/json; charset=utf-8 + Content-Disposition: form-data; name=operations + + {"query":"query Op_6e1beb14_1($file: Upload!) {\n singleUpload(file: $file) {\n fileName\n contentType\n content\n }\n}","variables":{"file":null}} + --f56524ab-5626-4955-b296-234a097b44f6 + Content-Type: application/json; charset=utf-8 + Content-Disposition: form-data; name=map + + {"0":["variables.file"]} + --f56524ab-5626-4955-b296-234a097b44f6 + Content-Type: text/plain; charset=utf-8 + Content-Disposition: form-data; name=0; filename=test.txt; filename*=utf-8''test.txt + + abc + --f56524ab-5626-4955-b296-234a097b44f6-- + response: + results: + - | + { + "data": { + "singleUpload": { + "fileName": "test.txt", + "contentType": "text/plain; charset=utf-8", + "content": "abc" + } + } + } +operationPlan: + operation: + - document: | + query($file: Upload!) { + singleUpload(file: $file) { + fileName + contentType + content + } + } + hash: 6e1beb142822946e961b9f062df288a0 + searchSpace: 1 + expandedNodes: 1 + nodes: + - id: 1 + type: Operation + schema: A + operation: | + query Op_6e1beb14_1($file: Upload!) { + singleUpload(file: $file) { + fileName + contentType + content + } + } + forwardedVariables: + - file + requiresFileUpload: true