From 90fb39c9346d24599ec014fe5c742a29898553a2 Mon Sep 17 00:00:00 2001 From: Glen Date: Tue, 6 Oct 2026 14:29:34 +0200 Subject: [PATCH 1/2] Answer unsupported methods and media types in Azure Functions --- ...zureFunctionServiceCollectionExtensions.cs | 4 + .../IsolatedProcessEndToEndTests.cs | 159 ++++++++++++++++++ .../InProcessEndToEndTests.cs | 137 +++++++++++++++ templates/azure-function/GraphQLFunction.cs | 2 +- .../migrating/migrate-from-16-6-to-16-7.md | 2 +- .../hotchocolate/server/http-transport.md | 2 + 6 files changed, 304 insertions(+), 2 deletions(-) diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs index c9e2ff5ea8f..7cb1fab0904 100644 --- a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs +++ b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs @@ -105,6 +105,10 @@ private static IServiceCollection AddAzureFunctionsGraphQLRequestExecutor( .UseWhen( context => context.MayReachNitroApp(path), branch => branch.UseNitroApp(path, serverOptions.Tool)) + .Use(MiddlewareFactory.CreateHttpUnsupportedRequestMiddleware( + executor, + serverOptions, + path)) .Compile(sp); return new DefaultGraphQLRequestExecutor(pipeline); diff --git a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/IsolatedProcessEndToEndTests.cs b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/IsolatedProcessEndToEndTests.cs index 564bd59fd42..f5041e8ee30 100644 --- a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/IsolatedProcessEndToEndTests.cs +++ b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/IsolatedProcessEndToEndTests.cs @@ -1,5 +1,7 @@ using System.Net; using System.Text; +using HotChocolate.AspNetCore; +using HotChocolate.AspNetCore.Formatters; using HotChocolate.AzureFunctions.IsolatedProcess.Tests.Helpers; using HotChocolate.AzureFunctions.Tests.Helpers; using HotChocolate.Types; @@ -7,6 +9,7 @@ using Microsoft.Azure.Functions.Extensions.DependencyInjection; using Microsoft.Azure.Functions.Worker.Http; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; using Newtonsoft.Json.Linq; namespace HotChocolate.AzureFunctions.IsolatedProcess.Tests; @@ -232,6 +235,162 @@ public async Task AzFuncIsolatedProcess_NitroTestAsync() Assert.True(resultContent.Contains("")); } + [Theory] + [InlineData(HttpTransportVersion.Legacy, HttpStatusCode.NotFound, null)] + [InlineData(HttpTransportVersion.Draft20250508, HttpStatusCode.NotFound, null)] + [InlineData( + HttpTransportVersion.Draft20260903, + HttpStatusCode.MethodNotAllowed, + "GET, HEAD, OPTIONS, POST")] + public async Task Put_Should_ReturnMethodNotAllowed_When_MethodIsUnsupported( + HttpTransportVersion transportVersion, + HttpStatusCode expectedStatusCode, + string? expectedAllow) + { + // arrange + var host = CreateHost(transportVersion); + var requestExecutor = host.Services.GetRequiredService(); + var request = new MockHttpRequestData( + new MockFunctionContext(host.Services), + HttpMethods.Put, + TestHttpContextHelper.DefaultAzFuncGraphQLUri, + """{"query":"{ person }"}"""); + + // act + var response = await requestExecutor.ExecuteAsync(request); + + // assert + Assert.Equal(expectedStatusCode, response.StatusCode); + Assert.Equal(expectedAllow, GetAllow(response)); + Assert.Equal(0, response.Body.Length); + } + + [Theory] + [InlineData(HttpTransportVersion.Legacy, HttpStatusCode.NotFound, null)] + [InlineData(HttpTransportVersion.Draft20250508, HttpStatusCode.NotFound, null)] + [InlineData( + HttpTransportVersion.Draft20260903, + HttpStatusCode.NoContent, + "GET, HEAD, OPTIONS, POST")] + public async Task Options_Should_ReturnAllowedMethods_When_EndpointIsRequested( + HttpTransportVersion transportVersion, + HttpStatusCode expectedStatusCode, + string? expectedAllow) + { + // arrange + var host = CreateHost(transportVersion); + var requestExecutor = host.Services.GetRequiredService(); + var request = new MockHttpRequestData( + new MockFunctionContext(host.Services), + HttpMethods.Options, + TestHttpContextHelper.DefaultAzFuncGraphQLUri); + + // act + var response = await requestExecutor.ExecuteAsync(request); + + // assert + Assert.Equal(expectedStatusCode, response.StatusCode); + Assert.Equal(expectedAllow, GetAllow(response)); + } + + [Theory] + [InlineData(HttpTransportVersion.Legacy, HttpStatusCode.NotFound)] + [InlineData(HttpTransportVersion.Draft20250508, HttpStatusCode.NotFound)] + [InlineData(HttpTransportVersion.Draft20260903, HttpStatusCode.UnsupportedMediaType)] + public async Task Post_Should_ReturnUnsupportedMediaType_When_ContentTypeIsUnsupported( + HttpTransportVersion transportVersion, + HttpStatusCode expectedStatusCode) + { + // arrange + var host = CreateHost(transportVersion); + var requestExecutor = host.Services.GetRequiredService(); + var request = new MockHttpRequestData( + new MockFunctionContext(host.Services), + HttpMethods.Post, + TestHttpContextHelper.DefaultAzFuncGraphQLUri, + "{ person }", + "text/plain"); + + // act + var response = await requestExecutor.ExecuteAsync(request); + + // assert + Assert.Equal(expectedStatusCode, response.StatusCode); + Assert.Equal(0, response.Body.Length); + } + + [Theory] + [InlineData(HttpTransportVersion.Legacy, HttpStatusCode.NotFound, null)] + [InlineData(HttpTransportVersion.Draft20250508, HttpStatusCode.NotFound, null)] + [InlineData( + HttpTransportVersion.Draft20260903, + HttpStatusCode.MethodNotAllowed, + "OPTIONS, POST")] + public async Task Get_Should_ReturnMethodNotAllowed_When_GetRequestsAreDisabled( + HttpTransportVersion transportVersion, + HttpStatusCode expectedStatusCode, + string? expectedAllow) + { + // arrange + var host = CreateHost(transportVersion, enableGetRequests: false); + var requestExecutor = host.Services.GetRequiredService(); + var request = new MockHttpRequestData( + new MockFunctionContext(host.Services), + HttpMethods.Get, + new Uri( + TestHttpContextHelper.DefaultAzFuncGraphQLUri, + "?query=%7B%20person%20%7D")); + + // act + var response = await requestExecutor.ExecuteAsync(request); + + // assert + Assert.Equal(expectedStatusCode, response.StatusCode); + Assert.Equal(expectedAllow, GetAllow(response)); + } + + [Fact] + public async Task Put_Should_ReturnNotFound_When_PathIsBelowTheFunctionRoute() + { + // arrange + var host = CreateHost(HttpTransportVersion.Draft20260903); + var requestExecutor = host.Services.GetRequiredService(); + var request = new MockHttpRequestData( + new MockFunctionContext(host.Services), + HttpMethods.Put, + new Uri(TestHttpContextHelper.DefaultAzFuncGraphQLUri, "/api/graphql/other"), + """{"query":"{ person }"}"""); + + // act + var response = await requestExecutor.ExecuteAsync(request); + + // assert + Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); + } + + private static IHost CreateHost( + HttpTransportVersion transportVersion, + bool enableGetRequests = true) + { + var formatterOptions = new HttpResponseFormatterOptions + { + HttpTransportVersion = transportVersion + }; + + return new MockIsolatedProcessHostBuilder() + .AddGraphQLFunction( + b => b + .AddQueryType(d => d.Name("Query").Field("person").Resolve("Luke Skywalker")) + .AddHttpResponseFormatter(formatterOptions) + .ModifyFunctionOptions(o => o.EnableGetRequests = enableGetRequests)) + .Build(); + } + + private static string? GetAllow(HttpResponseData response) + => response.Headers.TryGetValues("Allow", out var values) + ? string.Join(", ", values) + : null; + private static async Task ReadResponseAsStringAsync(HttpResponseData responseData) { responseData.Body.Seek(0, SeekOrigin.Begin); diff --git a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/InProcessEndToEndTests.cs b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/InProcessEndToEndTests.cs index c5234fd1f2f..ba1a1ca30cd 100644 --- a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/InProcessEndToEndTests.cs +++ b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/InProcessEndToEndTests.cs @@ -1,3 +1,5 @@ +using HotChocolate.AspNetCore; +using HotChocolate.AspNetCore.Formatters; using HotChocolate.AzureFunctions.Tests.Helpers; using HotChocolate.Types; using Microsoft.AspNetCore.Http; @@ -178,4 +180,139 @@ public async Task AzFuncInProcess_NitroTestAsync() Assert.False(string.IsNullOrWhiteSpace(resultContent)); Assert.True(resultContent.Contains("")); } + + [Theory] + [InlineData(HttpTransportVersion.Legacy, StatusCodes.Status404NotFound, null)] + [InlineData(HttpTransportVersion.Draft20250508, StatusCodes.Status404NotFound, null)] + [InlineData( + HttpTransportVersion.Draft20260903, + StatusCodes.Status405MethodNotAllowed, + "GET, HEAD, OPTIONS, POST")] + public async Task Put_Should_ReturnMethodNotAllowed_When_MethodIsUnsupported( + HttpTransportVersion transportVersion, + int expectedStatusCode, + string? expectedAllow) + { + // arrange + var requestExecutor = CreateRequestExecutor(transportVersion); + var httpContext = TestHttpContextHelper.NewGraphQLHttpContext("{ person }"); + httpContext.Request.Method = HttpMethods.Put; + + // act + await requestExecutor.ExecuteAsync(httpContext.Request); + + // assert + Assert.Equal(expectedStatusCode, httpContext.Response.StatusCode); + Assert.Equal(expectedAllow, httpContext.Response.Headers.Allow); + Assert.Equal(0, httpContext.Response.Body.Length); + } + + [Theory] + [InlineData(HttpTransportVersion.Legacy, StatusCodes.Status404NotFound, null)] + [InlineData(HttpTransportVersion.Draft20250508, StatusCodes.Status404NotFound, null)] + [InlineData( + HttpTransportVersion.Draft20260903, + StatusCodes.Status204NoContent, + "GET, HEAD, OPTIONS, POST")] + public async Task Options_Should_ReturnAllowedMethods_When_EndpointIsRequested( + HttpTransportVersion transportVersion, + int expectedStatusCode, + string? expectedAllow) + { + // arrange + var requestExecutor = CreateRequestExecutor(transportVersion); + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = HttpMethods.Options; + httpContext.Request.Path = TestHttpContextHelper.DefaultAzFuncGraphQLUri.AbsolutePath; + httpContext.Response.Body = new MemoryStream(); + + // act + await requestExecutor.ExecuteAsync(httpContext.Request); + + // assert + Assert.Equal(expectedStatusCode, httpContext.Response.StatusCode); + Assert.Equal(expectedAllow, httpContext.Response.Headers.Allow); + } + + [Theory] + [InlineData(HttpTransportVersion.Legacy, StatusCodes.Status404NotFound)] + [InlineData(HttpTransportVersion.Draft20250508, StatusCodes.Status404NotFound)] + [InlineData(HttpTransportVersion.Draft20260903, StatusCodes.Status415UnsupportedMediaType)] + public async Task Post_Should_ReturnUnsupportedMediaType_When_ContentTypeIsUnsupported( + HttpTransportVersion transportVersion, + int expectedStatusCode) + { + // arrange + var requestExecutor = CreateRequestExecutor(transportVersion); + var httpContext = TestHttpContextHelper.NewGraphQLHttpContext("{ person }"); + httpContext.Request.ContentType = "text/plain"; + + // act + await requestExecutor.ExecuteAsync(httpContext.Request); + + // assert + Assert.Equal(expectedStatusCode, httpContext.Response.StatusCode); + Assert.Equal(0, httpContext.Response.Body.Length); + } + + [Theory] + [InlineData(HttpTransportVersion.Legacy, StatusCodes.Status404NotFound, null)] + [InlineData(HttpTransportVersion.Draft20250508, StatusCodes.Status404NotFound, null)] + [InlineData( + HttpTransportVersion.Draft20260903, + StatusCodes.Status405MethodNotAllowed, + "OPTIONS, POST")] + public async Task Get_Should_ReturnMethodNotAllowed_When_GetRequestsAreDisabled( + HttpTransportVersion transportVersion, + int expectedStatusCode, + string? expectedAllow) + { + // arrange + var requestExecutor = CreateRequestExecutor(transportVersion, enableGetRequests: false); + var httpContext = new DefaultHttpContext(); + httpContext.Request.Method = HttpMethods.Get; + httpContext.Request.Path = TestHttpContextHelper.DefaultAzFuncGraphQLUri.AbsolutePath; + httpContext.Request.QueryString = new QueryString("?query=%7B%20person%20%7D"); + httpContext.Response.Body = new MemoryStream(); + + // act + await requestExecutor.ExecuteAsync(httpContext.Request); + + // assert + Assert.Equal(expectedStatusCode, httpContext.Response.StatusCode); + Assert.Equal(expectedAllow, httpContext.Response.Headers.Allow); + } + + [Fact] + public async Task Put_Should_ReturnNotFound_When_PathIsBelowTheFunctionRoute() + { + // arrange + var requestExecutor = CreateRequestExecutor(HttpTransportVersion.Draft20260903); + var httpContext = TestHttpContextHelper.NewGraphQLHttpContext("{ person }"); + httpContext.Request.Method = HttpMethods.Put; + httpContext.Request.Path = "/api/graphql/other"; + + // act + await requestExecutor.ExecuteAsync(httpContext.Request); + + // assert + Assert.Equal(StatusCodes.Status404NotFound, httpContext.Response.StatusCode); + } + + private static IGraphQLRequestExecutor CreateRequestExecutor( + HttpTransportVersion transportVersion, + bool enableGetRequests = true) + { + var hostBuilder = new MockInProcessFunctionsHostBuilder(); + hostBuilder + .AddGraphQLFunction() + .AddQueryType(d => d.Name("Query").Field("person").Resolve("Luke Skywalker")) + .AddHttpResponseFormatter( + new HttpResponseFormatterOptions { HttpTransportVersion = transportVersion }) + .ModifyFunctionOptions(o => o.EnableGetRequests = enableGetRequests); + + return hostBuilder + .BuildServiceProvider() + .GetRequiredService(); + } } diff --git a/templates/azure-function/GraphQLFunction.cs b/templates/azure-function/GraphQLFunction.cs index dffa0d67b1b..103ec5ea256 100644 --- a/templates/azure-function/GraphQLFunction.cs +++ b/templates/azure-function/GraphQLFunction.cs @@ -15,7 +15,7 @@ public GraphQLFunction(IGraphQLRequestExecutor executor) [Function("GraphQLHttpFunction")] public Task Run( - [HttpTrigger(AuthorizationLevel.Anonymous, "get", "post", Route = "graphql/{**slug}")] + [HttpTrigger(AuthorizationLevel.Anonymous, Route = "graphql/{**slug}")] HttpRequestData request) => _executor.ExecuteAsync(request); } diff --git a/website/content/docs/hotchocolate/migrating/migrate-from-16-6-to-16-7.md b/website/content/docs/hotchocolate/migrating/migrate-from-16-6-to-16-7.md index e6cdd1eab4f..3055064d8ce 100644 --- a/website/content/docs/hotchocolate/migrating/migrate-from-16-6-to-16-7.md +++ b/website/content/docs/hotchocolate/migrating/migrate-from-16-6-to-16-7.md @@ -295,7 +295,7 @@ app.MapHealthChecks("/health"); app.MapGraphQL(); ``` -In Azure Functions, which serves the embedded Nitro, a request whose `Accept` header contains `text/html` without preferring it, such as `text/html;q=0`, gets `404` and not the Nitro page. +In Azure Functions, which serves the embedded Nitro, a request whose `Accept` header contains `text/html` without preferring it, such as `text/html;q=0`, is not served the Nitro page: it has a `404` status code, or a `405` status code when GET requests are disabled under the `Draft20260903` transport version. # Noteworthy changes diff --git a/website/content/docs/hotchocolate/server/http-transport.md b/website/content/docs/hotchocolate/server/http-transport.md index b59abc2fd14..4f78a1bf249 100644 --- a/website/content/docs/hotchocolate/server/http-transport.md +++ b/website/content/docs/hotchocolate/server/http-transport.md @@ -413,6 +413,8 @@ A value outside this list throws an `ArgumentOutOfRangeException` when the forma - A request on the GraphQL endpoint whose method the endpoint does not support has a `405` status code and an `Allow` header listing the supported methods, an `OPTIONS` request has a `204` status code with the same header, and a `POST` request whose `Content-Type` the endpoint does not support has a `415` status code. Under `Draft20250508`, all three have a `404` status code. - When `EnableQueryRequests` is `true`, the `Allow` header lists `QUERY`, the `405`, `OPTIONS`, and `415` responses carry `Accept-Query: application/json`, and a `QUERY` request whose `Content-Type` the endpoint does not support has a `415` status code. Under `Draft20250508`, that request has a `404` status code. +In Azure Functions, a request reaches Hot Chocolate only with a method the function's `HttpTrigger` accepts, and the Functions host gives any other method a `404` status code. A trigger that lists no methods, as the one in the `graphql-azf` template does, passes every method to Hot Chocolate. + > [!NOTE] > `294` is not registered with IANA. Clients and intermediaries that do not recognize it treat it as `200` per RFC 9110, and it is not heuristically cacheable, so a response without cache headers is not stored. Infrastructure that acts on a fixed list of status codes can still treat it differently from `200`. nginx's `add_header` directive, for example, emits headers only for a fixed list of codes unless the `always` flag is set, so CORS and security headers added that way are missing on a `294` response. Before enabling `Draft20260903`, verify that headers and caching behave as intended for `294` through your own infrastructure. From c545b1ecc38227e4a9bcf6ac92df1130a2e172ea Mon Sep 17 00:00:00 2001 From: Glen Date: Tue, 6 Oct 2026 15:03:23 +0200 Subject: [PATCH 2/2] Test QUERY requests in the Azure Functions pipeline --- .../IsolatedProcessEndToEndTests.cs | 73 +++++++++++++++++-- .../InProcessEndToEndTests.cs | 57 ++++++++++++++- 2 files changed, 121 insertions(+), 9 deletions(-) diff --git a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/IsolatedProcessEndToEndTests.cs b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/IsolatedProcessEndToEndTests.cs index f5041e8ee30..c46e6a37162 100644 --- a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/IsolatedProcessEndToEndTests.cs +++ b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/IsolatedProcessEndToEndTests.cs @@ -261,7 +261,7 @@ public async Task Put_Should_ReturnMethodNotAllowed_When_MethodIsUnsupported( // assert Assert.Equal(expectedStatusCode, response.StatusCode); - Assert.Equal(expectedAllow, GetAllow(response)); + Assert.Equal(expectedAllow, GetHeader(response, "Allow")); Assert.Equal(0, response.Body.Length); } @@ -290,7 +290,7 @@ public async Task Options_Should_ReturnAllowedMethods_When_EndpointIsRequested( // assert Assert.Equal(expectedStatusCode, response.StatusCode); - Assert.Equal(expectedAllow, GetAllow(response)); + Assert.Equal(expectedAllow, GetHeader(response, "Allow")); } [Theory] @@ -346,7 +346,7 @@ public async Task Get_Should_ReturnMethodNotAllowed_When_GetRequestsAreDisabled( // assert Assert.Equal(expectedStatusCode, response.StatusCode); - Assert.Equal(expectedAllow, GetAllow(response)); + Assert.Equal(expectedAllow, GetHeader(response, "Allow")); } [Fact] @@ -368,9 +368,63 @@ public async Task Put_Should_ReturnNotFound_When_PathIsBelowTheFunctionRoute() Assert.Equal(HttpStatusCode.NotFound, response.StatusCode); } + [Theory] + [InlineData(HttpTransportVersion.Legacy, HttpStatusCode.NotFound, null)] + [InlineData(HttpTransportVersion.Draft20250508, HttpStatusCode.NotFound, null)] + [InlineData( + HttpTransportVersion.Draft20260903, + HttpStatusCode.UnsupportedMediaType, + "application/json")] + public async Task Query_Should_ReturnUnsupportedMediaType_When_ContentTypeIsUnsupported( + HttpTransportVersion transportVersion, + HttpStatusCode expectedStatusCode, + string? expectedAcceptQuery) + { + // arrange + var host = CreateHost(transportVersion, enableQueryRequests: true); + var requestExecutor = host.Services.GetRequiredService(); + var request = new MockHttpRequestData( + new MockFunctionContext(host.Services), + "QUERY", + TestHttpContextHelper.DefaultAzFuncGraphQLUri, + "{ person }", + "text/plain"); + + // act + var response = await requestExecutor.ExecuteAsync(request); + + // assert + Assert.Equal(expectedStatusCode, response.StatusCode); + Assert.Equal(expectedAcceptQuery, GetHeader(response, "Accept-Query")); + Assert.Equal(0, response.Body.Length); + } + + [Fact] + public async Task Query_Should_ExecuteRequest_When_QueryRequestsAreEnabled() + { + // arrange + var host = CreateHost(HttpTransportVersion.Latest, enableQueryRequests: true); + var requestExecutor = host.Services.GetRequiredService(); + var request = new MockHttpRequestData( + new MockFunctionContext(host.Services), + "QUERY", + TestHttpContextHelper.DefaultAzFuncGraphQLUri, + """{"query":"{ person }"}"""); + + // act + var response = await requestExecutor.ExecuteAsync(request); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal( + """{"data":{"person":"Luke Skywalker"}}""", + await ReadResponseAsStringAsync(response)); + } + private static IHost CreateHost( HttpTransportVersion transportVersion, - bool enableGetRequests = true) + bool enableGetRequests = true, + bool enableQueryRequests = false) { var formatterOptions = new HttpResponseFormatterOptions { @@ -382,12 +436,17 @@ private static IHost CreateHost( b => b .AddQueryType(d => d.Name("Query").Field("person").Resolve("Luke Skywalker")) .AddHttpResponseFormatter(formatterOptions) - .ModifyFunctionOptions(o => o.EnableGetRequests = enableGetRequests)) + .ModifyFunctionOptions( + o => + { + o.EnableGetRequests = enableGetRequests; + o.EnableQueryRequests = enableQueryRequests; + })) .Build(); } - private static string? GetAllow(HttpResponseData response) - => response.Headers.TryGetValues("Allow", out var values) + private static string? GetHeader(HttpResponseData response, string name) + => response.Headers.TryGetValues(name, out var values) ? string.Join(", ", values) : null; diff --git a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/InProcessEndToEndTests.cs b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/InProcessEndToEndTests.cs index ba1a1ca30cd..c5ec61e4e01 100644 --- a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/InProcessEndToEndTests.cs +++ b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/InProcessEndToEndTests.cs @@ -299,9 +299,57 @@ public async Task Put_Should_ReturnNotFound_When_PathIsBelowTheFunctionRoute() Assert.Equal(StatusCodes.Status404NotFound, httpContext.Response.StatusCode); } + [Theory] + [InlineData(HttpTransportVersion.Legacy, StatusCodes.Status404NotFound, null)] + [InlineData(HttpTransportVersion.Draft20250508, StatusCodes.Status404NotFound, null)] + [InlineData( + HttpTransportVersion.Draft20260903, + StatusCodes.Status415UnsupportedMediaType, + "application/json")] + public async Task Query_Should_ReturnUnsupportedMediaType_When_ContentTypeIsUnsupported( + HttpTransportVersion transportVersion, + int expectedStatusCode, + string? expectedAcceptQuery) + { + // arrange + var requestExecutor = CreateRequestExecutor(transportVersion, enableQueryRequests: true); + var httpContext = TestHttpContextHelper.NewGraphQLHttpContext("{ person }"); + httpContext.Request.Method = "QUERY"; + httpContext.Request.ContentType = "text/plain"; + + // act + await requestExecutor.ExecuteAsync(httpContext.Request); + + // assert + Assert.Equal(expectedStatusCode, httpContext.Response.StatusCode); + Assert.Equal(expectedAcceptQuery, httpContext.Response.Headers["Accept-Query"]); + Assert.Equal(0, httpContext.Response.Body.Length); + } + + [Fact] + public async Task Query_Should_ExecuteRequest_When_QueryRequestsAreEnabled() + { + // arrange + var requestExecutor = CreateRequestExecutor( + HttpTransportVersion.Latest, + enableQueryRequests: true); + var httpContext = TestHttpContextHelper.NewGraphQLHttpContext("{ person }"); + httpContext.Request.Method = "QUERY"; + + // act + await requestExecutor.ExecuteAsync(httpContext.Request); + + // assert + Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode); + Assert.Equal( + """{"data":{"person":"Luke Skywalker"}}""", + await httpContext.ReadResponseContentAsync()); + } + private static IGraphQLRequestExecutor CreateRequestExecutor( HttpTransportVersion transportVersion, - bool enableGetRequests = true) + bool enableGetRequests = true, + bool enableQueryRequests = false) { var hostBuilder = new MockInProcessFunctionsHostBuilder(); hostBuilder @@ -309,7 +357,12 @@ private static IGraphQLRequestExecutor CreateRequestExecutor( .AddQueryType(d => d.Name("Query").Field("person").Resolve("Luke Skywalker")) .AddHttpResponseFormatter( new HttpResponseFormatterOptions { HttpTransportVersion = transportVersion }) - .ModifyFunctionOptions(o => o.EnableGetRequests = enableGetRequests); + .ModifyFunctionOptions( + o => + { + o.EnableGetRequests = enableGetRequests; + o.EnableQueryRequests = enableQueryRequests; + }); return hostBuilder .BuildServiceProvider()