From 716b1975444d0749d803e6aaf7a01e3734e92aaa Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Mon, 19 Oct 2026 14:37:43 +0000 Subject: [PATCH 1/6] Adopt the slug-based Nitro app middleware ChilliCream.Nitro.App now dispatches on the slug route value instead of matching a configured path, so it works under MapGroup. Drop the path argument from UseNitroApp, and for hosts without endpoint routing (Azure Functions and IApplicationBuilder.MapGraphQL used as plain middleware) set the slug from the GraphQL path before invoking NitroAppMiddleware. --- .../EndpointRouteBuilderExtensions.cs | 15 +++++- .../Extensions/HttpContextExtensions.cs | 16 ++++++ ...zureFunctionServiceCollectionExtensions.cs | 51 +++---------------- 3 files changed, 36 insertions(+), 46 deletions(-) diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/EndpointRouteBuilderExtensions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/EndpointRouteBuilderExtensions.cs index 140b3e88c28..eb72cb737e3 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/EndpointRouteBuilderExtensions.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/EndpointRouteBuilderExtensions.cs @@ -144,7 +144,18 @@ public static IApplicationBuilder MapGraphQL( executor, serverOptions, path, MiddlewareRoutingType.Integrated)) .UseWhen( context => context.MayReachNitroApp(path), - branch => branch.UseNitroApp(path, serverOptions.Tool)) + branch => branch + .Use(next => context => + { + // Endpoint routing provides the slug; without it, derive it from the path. + if (context.GetEndpoint() is null) + { + context.SetNitroAppSlug(path); + } + + return next(context); + }) + .UseNitroApp(serverOptions.Tool)) .Use(MiddlewareFactory.CreateHttpUnsupportedRequestMiddleware( executor, serverOptions, @@ -529,7 +540,7 @@ public static NitroAppEndpointConventionBuilder MapNitroApp( var nitroOptions = new NitroAppOptions { GraphQLEndpoint = relativeRequestPath }; requestPipeline - .UseNitroApp(toolPath, nitroOptions) + .UseNitroApp(nitroOptions) .Use(_ => context => { context.Response.StatusCode = 404; diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpContextExtensions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpContextExtensions.cs index 08ee1cad817..8837956bfdc 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpContextExtensions.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpContextExtensions.cs @@ -1,3 +1,4 @@ +using ChilliCream.Nitro.App; using HotChocolate.AspNetCore.Formatters; using HotChocolate.AspNetCore.Utilities; using Microsoft.AspNetCore.Http; @@ -40,6 +41,21 @@ public static bool MayReachNitroApp(this HttpContext context, PathString path) && DefaultHttpResponseFormatter.PrefersHtml(acceptHeader.AcceptMediaTypes); } + /// + /// Sets the Nitro app slug route value for a request below , for + /// hosts that mount the Nitro app without endpoint routing. + /// + public static void SetNitroAppSlug(this HttpContext context, PathString path) + { + var request = context.Request; + + if (request.Path.StartsWithSegments(path, StringComparison.OrdinalIgnoreCase, out var remaining)) + { + request.RouteValues[NitroAppMiddleware.SlugRouteValueName] = + remaining.Value?.TrimStart('/') ?? string.Empty; + } + } + public static string? TryGetCostSwitch(this HttpContext context) { var headers = context.Request.Headers; diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs index 7cb1fab0904..bf9ca1b71bf 100644 --- a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs +++ b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs @@ -8,7 +8,6 @@ using Microsoft.AspNetCore.Http.Features; using Microsoft.Azure.WebJobs.Host.Config; using Microsoft.Extensions.DependencyInjection.Extensions; -using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Options; using MiddlewareFactory = HotChocolate.AspNetCore.MiddlewareFactory; @@ -104,7 +103,13 @@ private static IServiceCollection AddAzureFunctionsGraphQLRequestExecutor( .Use(MiddlewareFactory.CreateHttpGetSchemaMiddleware(executor, serverOptions, path, MiddlewareRoutingType.Integrated)) .UseWhen( context => context.MayReachNitroApp(path), - branch => branch.UseNitroApp(path, serverOptions.Tool)) + branch => branch + .Use(next => context => + { + context.SetNitroAppSlug(path); + return next(context); + }) + .Use(NitroAppMiddleware.Create(serverOptions.Tool))) .Use(MiddlewareFactory.CreateHttpUnsupportedRequestMiddleware( executor, serverOptions, @@ -139,46 +144,4 @@ public static IRequestExecutorBuilder ModifyFunctionOptions( builder.ModifyServerOptions(configure); return builder; } - - private static PipelineBuilder UseNitroApp( - this PipelineBuilder requestPipeline, - PathString path, - NitroAppOptions options) - { - ArgumentNullException.ThrowIfNull(requestPipeline); - - path = path.ToString().TrimEnd('/'); - var fileProvider = CreateFileProvider(); - var forwarderAccessor = new HttpForwarderAccessor(); - - return requestPipeline - .Use(next => - { - var middleware = new NitroAppOptionsFileMiddleware(next, path, options); - return middleware.Invoke; - }) - .Use(next => - { - var middleware = new NitroAppCdnMiddleware(next, path, forwarderAccessor, options); - return middleware.Invoke; - }) - .Use(next => - { - var middleware = new NitroAppDefaultFileMiddleware(next, fileProvider, path, options); - return middleware.Invoke; - }) - .Use(next => - { - var middleware = new NitroAppStaticFileMiddleware(next, fileProvider, path, options); - return middleware.Invoke; - }); - } - - private static IFileProvider CreateFileProvider() - { - var type = typeof(NitroAppStaticFileMiddleware); - var resourceNamespace = type.Namespace + ".Resources"; - - return new EmbeddedFileProvider(type.Assembly, resourceNamespace); - } } From 9c62cfd857b3c9014f4117dbf68c4178a1a8b80f Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Mon, 19 Oct 2026 15:29:48 +0000 Subject: [PATCH 2/6] Serve the embedded Nitro app from ChilliCream.Nitro.App.Embed ChilliCream.Nitro.App no longer contains the embedded Nitro app; it moved to the ChilliCream.Nitro.App.Embed package. Reference the embed package and pass its embedded files to the Nitro middleware, so ServeMode.Embedded keeps working for MapGraphQL, MapNitroApp and Azure Functions. The embed package also adds WithEmbeddedNitroAssets(), which serves the embedded Nitro app on a GraphQL endpoint, a Nitro app endpoint or a route group. Add tests for it and document it, including that ServeMode.Embedded is removed in Hot Chocolate 17. --- src/Directory.Packages.props | 1 + .../EndpointRouteBuilderExtensions.cs | 5 +- .../HotChocolate.AspNetCore.Pipeline.csproj | 1 + .../AspNetCore/HotChocolate.AspNetCore.csproj | 1 + .../AspNetCore.Tests/EmbeddedNitroAppTests.cs | 228 ++++++++++++++++++ ...zureFunctionServiceCollectionExtensions.cs | 5 +- .../HotChocolate.AzureFunctions.csproj | 1 + .../docs/hotchocolate/server/endpoints.md | 8 + .../docs/nitro/integrations/hot-chocolate.md | 36 ++- 9 files changed, 276 insertions(+), 10 deletions(-) create mode 100644 src/HotChocolate/AspNetCore/test/AspNetCore.Tests/EmbeddedNitroAppTests.cs diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 39d66005a0f..ebf0efcb3af 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -20,6 +20,7 @@ + diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/EndpointRouteBuilderExtensions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/EndpointRouteBuilderExtensions.cs index eb72cb737e3..746c4f96e8b 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/EndpointRouteBuilderExtensions.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/EndpointRouteBuilderExtensions.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using ChilliCream.Nitro.App; +using ChilliCream.Nitro.App.Embed; using HotChocolate.AspNetCore; using HotChocolate.AspNetCore.Extensions; using Microsoft.AspNetCore.Http; @@ -155,7 +156,7 @@ public static IApplicationBuilder MapGraphQL( return next(context); }) - .UseNitroApp(serverOptions.Tool)) + .UseNitroApp(serverOptions.Tool, EmbeddedNitroApp.AssetSource)) .Use(MiddlewareFactory.CreateHttpUnsupportedRequestMiddleware( executor, serverOptions, @@ -540,7 +541,7 @@ public static NitroAppEndpointConventionBuilder MapNitroApp( var nitroOptions = new NitroAppOptions { GraphQLEndpoint = relativeRequestPath }; requestPipeline - .UseNitroApp(nitroOptions) + .UseNitroApp(nitroOptions, EmbeddedNitroApp.AssetSource) .Use(_ => context => { context.Response.StatusCode = 404; diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HotChocolate.AspNetCore.Pipeline.csproj b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HotChocolate.AspNetCore.Pipeline.csproj index 10fcfa74bad..ad420c71668 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HotChocolate.AspNetCore.Pipeline.csproj +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/HotChocolate.AspNetCore.Pipeline.csproj @@ -20,6 +20,7 @@ + diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore/HotChocolate.AspNetCore.csproj b/src/HotChocolate/AspNetCore/src/AspNetCore/HotChocolate.AspNetCore.csproj index 225d56f33cc..346fddee503 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore/HotChocolate.AspNetCore.csproj +++ b/src/HotChocolate/AspNetCore/src/AspNetCore/HotChocolate.AspNetCore.csproj @@ -27,6 +27,7 @@ + diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/EmbeddedNitroAppTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/EmbeddedNitroAppTests.cs new file mode 100644 index 00000000000..a6371a476c9 --- /dev/null +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/EmbeddedNitroAppTests.cs @@ -0,0 +1,228 @@ +using System.Net; +using System.Net.Http.Headers; +using ChilliCream.Nitro.App; +using HotChocolate.AspNetCore.Tests.Utilities; +using HotChocolate.StarWars; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Yarp.ReverseProxy.Forwarder; + +namespace HotChocolate.AspNetCore; + +public class EmbeddedNitroAppTests : ServerTestBase +{ + public EmbeddedNitroAppTests(TestServerFactory serverFactory) + : base(serverFactory) + { + } + + [Fact] + public async Task MapGraphQL_WithEmbeddedNitroAssets_Serves_Nitro_And_GraphQL_On_The_Same_Path() + { + // arrange + var forwarder = new RecordingForwarder(); + var server = CreateServer(forwarder, e => e.MapGraphQL().WithEmbeddedNitroAssets()); + + // act + var nitro = await GetHtmlAsync(server, "/graphql/"); + var graphql = await server.PostAsync(new ClientQueryRequest { Query = "{ __typename }" }); + + // assert + Assert.Equal(HttpStatusCode.OK, nitro.StatusCode); + Assert.Equal("text/html", nitro.Content.Headers.ContentType?.MediaType); + Assert.Null(forwarder.DestinationPrefix); + Assert.Equal(HttpStatusCode.OK, graphql.StatusCode); + Assert.Equal("Query", graphql.Data?["__typename"]); + } + + [Fact] + public async Task MapNitroApp_WithEmbeddedNitroAssets_Serves_Nitro_And_Config() + { + // arrange + var forwarder = new RecordingForwarder(); + var server = CreateServer( + forwarder, + e => e.MapNitroApp().WithOptions(o => o.Title = "Embedded").WithEmbeddedNitroAssets()); + + // act + var nitro = await GetHtmlAsync(server, "/graphql/ui/"); + var config = await server.CreateClient().GetStringAsync( + "/graphql/ui/nitro-config.json", + TestContext.Current.CancellationToken); + + // assert + Assert.Equal(HttpStatusCode.OK, nitro.StatusCode); + Assert.Equal("text/html", nitro.Content.Headers.ContentType?.MediaType); + Assert.Null(forwarder.DestinationPrefix); + Assert.Contains("\"title\":\"Embedded\"", config); + } + + [Fact] + public async Task MapGraphQL_WithEmbeddedNitroAssets_In_MapGroup_Serves_Nitro_And_GraphQL() + { + // arrange + var forwarder = new RecordingForwarder(); + var server = CreateServer(forwarder, e => e.MapGroup("/api").MapGraphQL().WithEmbeddedNitroAssets()); + + // act + var nitro = await GetHtmlAsync(server, "/api/graphql/"); + var graphql = await server.PostAsync( + new ClientQueryRequest { Query = "{ __typename }" }, + "/api/graphql"); + + // assert + Assert.Equal(HttpStatusCode.OK, nitro.StatusCode); + Assert.Equal("text/html", nitro.Content.Headers.ContentType?.MediaType); + Assert.Null(forwarder.DestinationPrefix); + Assert.Equal("Query", graphql.Data?["__typename"]); + } + + [Fact] + public async Task MapGroup_WithEmbeddedNitroAssets_Serves_Nitro_For_Every_Endpoint_In_The_Group() + { + // arrange + var forwarder = new RecordingForwarder(); + var server = CreateServer( + forwarder, + e => + { + var group = e.MapGroup("/dev").WithEmbeddedNitroAssets(); + group.MapGraphQL(); + group.MapNitroApp("/ui"); + }); + + // act + var graphQLNitro = await GetHtmlAsync(server, "/dev/graphql/"); + var nitroApp = await GetHtmlAsync(server, "/dev/ui/"); + + // assert + Assert.Equal(HttpStatusCode.OK, graphQLNitro.StatusCode); + Assert.Equal(HttpStatusCode.OK, nitroApp.StatusCode); + Assert.Null(forwarder.DestinationPrefix); + } + + [Fact] + public async Task MapGroup_WithEmbeddedNitroAssets_Wins_Over_Schema_Wide_ServeMode() + { + // arrange + var forwarder = new RecordingForwarder(); + var server = ServerFactory.Create( + services => services + .AddSingleton(forwarder) + .AddRouting() + .AddGraphQLServer() + .AddStarWarsTypes() + .AddStarWarsRepositories() + .ModifyServerOptions(o => o.Tool.ServeMode = ServeMode.Insider), + app => app + .UseRouting() + .UseEndpoints(e => e.MapGroup("/dev").WithEmbeddedNitroAssets().MapGraphQL())); + + // act + var nitro = await GetHtmlAsync(server, "/dev/graphql/"); + + // assert + Assert.Equal(HttpStatusCode.OK, nitro.StatusCode); + Assert.Null(forwarder.DestinationPrefix); + } + + [Fact] + public async Task MapGraphQL_With_ServeMode_Embedded_Still_Serves_Embedded_Nitro() + { + // arrange + var forwarder = new RecordingForwarder(); + var server = CreateServer( + forwarder, + e => e.MapGraphQL().WithOptions(o => o.Tool.ServeMode = ServeMode.Embedded)); + + // act + var nitro = await GetHtmlAsync(server, "/graphql/"); + + // assert + Assert.Equal(HttpStatusCode.OK, nitro.StatusCode); + Assert.Equal("text/html", nitro.Content.Headers.ContentType?.MediaType); + Assert.Null(forwarder.DestinationPrefix); + } + + [Fact] + public async Task MapGraphQL_Without_Embedded_Proxies_Nitro_From_The_Cdn() + { + // arrange + var forwarder = new RecordingForwarder(); + var server = CreateServer(forwarder, e => e.MapGraphQL()); + + // act + var nitro = await GetHtmlAsync(server, "/graphql/"); + + // assert + Assert.Equal(HttpStatusCode.OK, nitro.StatusCode); + Assert.Equal("https://cdn.chillicream.com/web/latest/", forwarder.DestinationPrefix); + } + + [Fact] + public async Task WithOptions_Version_After_WithEmbeddedNitroAssets_Proxies_That_Version() + { + // arrange + var forwarder = new RecordingForwarder(); + var server = CreateServer( + forwarder, + e => e.MapNitroApp() + .WithEmbeddedNitroAssets() + .WithOptions(o => o.ServeMode = ServeMode.Version("5.0.8"))); + + // act + var nitro = await GetHtmlAsync(server, "/graphql/ui/"); + + // assert + Assert.Equal(HttpStatusCode.OK, nitro.StatusCode); + Assert.Equal("https://cdn.chillicream.com/web/5.0.8/", forwarder.DestinationPrefix); + } + + private TestServer CreateServer(RecordingForwarder forwarder, Action configure) + => ServerFactory.Create( + services => services + .AddSingleton(forwarder) + .AddRouting() + .AddGraphQLServer() + .AddStarWarsTypes() + .AddStarWarsRepositories(), + app => app + .UseRouting() + .UseEndpoints(configure)); + + private static Task GetHtmlAsync(TestServer server, string path) + { + var request = new HttpRequestMessage(HttpMethod.Get, path); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/html")); + return server.CreateClient().SendAsync(request, TestContext.Current.CancellationToken); + } + + private sealed class RecordingForwarder : IHttpForwarder + { + public string? DestinationPrefix { get; private set; } + + public ValueTask SendAsync( + HttpContext context, + string destinationPrefix, + HttpMessageInvoker httpClient, + ForwarderRequestConfig requestConfig, + HttpTransformer transformer, + CancellationToken cancellationToken) + { + DestinationPrefix = destinationPrefix; + context.Response.StatusCode = StatusCodes.Status200OK; + return ValueTask.FromResult(ForwarderError.None); + } + + public ValueTask SendAsync( + HttpContext context, + string destinationPrefix, + HttpMessageInvoker httpClient, + ForwarderRequestConfig requestConfig, + HttpTransformer transformer) + => SendAsync(context, destinationPrefix, httpClient, requestConfig, transformer, default); + } +} diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs index bf9ca1b71bf..374e4e13e61 100644 --- a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs +++ b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ using ChilliCream.Nitro.App; +using ChilliCream.Nitro.App.Embed; using HotChocolate; using HotChocolate.AspNetCore; using HotChocolate.AzureFunctions; @@ -109,7 +110,9 @@ private static IServiceCollection AddAzureFunctionsGraphQLRequestExecutor( context.SetNitroAppSlug(path); return next(context); }) - .Use(NitroAppMiddleware.Create(serverOptions.Tool))) + .Use(NitroAppMiddleware.Create( + serverOptions.Tool, + embeddedAssetSource: EmbeddedNitroApp.AssetSource))) .Use(MiddlewareFactory.CreateHttpUnsupportedRequestMiddleware( executor, serverOptions, diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/HotChocolate.AzureFunctions.csproj b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/HotChocolate.AzureFunctions.csproj index b02bcc7e998..f833fb309b3 100644 --- a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/HotChocolate.AzureFunctions.csproj +++ b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/HotChocolate.AzureFunctions.csproj @@ -19,6 +19,7 @@ + diff --git a/website/content/docs/hotchocolate/server/endpoints.md b/website/content/docs/hotchocolate/server/endpoints.md index 75cb24b31cd..e0ad8cc28f7 100644 --- a/website/content/docs/hotchocolate/server/endpoints.md +++ b/website/content/docs/hotchocolate/server/endpoints.md @@ -139,6 +139,14 @@ app.UseEndpoints(endpoints => This makes Nitro accessible via a web browser at the `/graphql/ui` endpoint. +To serve the Nitro app that ships in the `ChilliCream.Nitro.App.Embed` package instead of serving it from the Nitro CDN, call `WithEmbeddedNitroAssets()`. This works on `MapNitroApp()`, on `MapGraphQL()` and on route groups. + +```csharp +endpoints.MapNitroApp("/graphql/ui").WithEmbeddedNitroAssets(); +``` + +[Learn more about serving the embedded Nitro app](../../nitro/integrations/hot-chocolate.md#serve-the-embedded-nitro-app) + ## NitroAppOptions You can configure Nitro using `NitroAppOptions`. diff --git a/website/content/docs/nitro/integrations/hot-chocolate.md b/website/content/docs/nitro/integrations/hot-chocolate.md index 83c2dd513ed..64d11cf1a3a 100644 --- a/website/content/docs/nitro/integrations/hot-chocolate.md +++ b/website/content/docs/nitro/integrations/hot-chocolate.md @@ -38,12 +38,10 @@ endpoints # Serve Modes -The `ServeMode` property controls which version of Nitro to serve. The default mode is `Latest`, serving the most recent version of Nitro from a CDN. -You can also serve the embedded version (`Embedded`) of Nitro, which is included in the package. +The `ServeMode` property controls which version of Nitro to serve from the Nitro CDN. The default mode is `Latest`, serving the most recent version of Nitro. -- `Latest`: Serves the latest version of Nitro from a CDN. -- `Insider`: Serves the insider version of Nitro from a CDN, allowing preview of upcoming features. -- `Embedded`: Serves the embedded version of Nitro that comes with the package. +- `Latest`: Serves the latest version of Nitro from the CDN. +- `Insider`: Serves the insider version of Nitro from the CDN, allowing preview of upcoming features. - `Version(string version)`: Serves a specific version of Nitro from the CDN. Depending on your environment or preferences, you can choose the appropriate mode: @@ -51,9 +49,33 @@ Depending on your environment or preferences, you can choose the appropriate mod ```csharp endpoints .MapNitroApp() - .WithOptions(o => o.ServeMode = ServeMode.Embedded); + .WithOptions(o => o.ServeMode = ServeMode.Insider); ``` +# Serve the Embedded Nitro App + +Instead of serving Nitro from the CDN, you can serve the version of Nitro that ships in the `ChilliCream.Nitro.App.Embed` package. Call `WithEmbeddedNitroAssets()` on the endpoint: + +```csharp +app.UseEndpoints(endpoints => +{ + endpoints.MapGraphQL().WithEmbeddedNitroAssets(); + endpoints.MapNitroApp("/my-graphql-ui").WithEmbeddedNitroAssets(); +}); +``` + +On `MapGraphQL()`, Nitro stays on the same path as the GraphQL endpoint. Call `WithEmbeddedNitroAssets()` on a route group to serve the embedded Nitro app on every endpoint in the group: + +```csharp +var api = app.MapGroup("/api").WithEmbeddedNitroAssets(); +api.MapGraphQL(); +``` + +All other Nitro options work the same way for the embedded Nitro app. As with other options that set the serve mode, the last call wins: calling `WithOptions(o => o.ServeMode = ServeMode.Version("..."))` after `WithEmbeddedNitroAssets()` serves that version from the CDN. + +> [!NOTE] +> Setting `ServeMode.Embedded` still serves the embedded Nitro app, but it will be removed in Hot Chocolate 17. From Hot Chocolate 17, reference the `ChilliCream.Nitro.App.Embed` package and call `WithEmbeddedNitroAssets()`. + # Configuration Options You can tailor Nitro to your needs by setting various options via `NitroAppOptions`. You can specify these options using the `WithOptions()` method in both `MapGraphQL()` and `MapNitroApp()` methods. @@ -61,7 +83,7 @@ You can tailor Nitro to your needs by setting various options via `NitroAppOptio | Property | Type | Description | | ------------------------------ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | Enable | `bool` | If `false`, disables the Nitro tool. | -| ServeMode | `ServeMode` | Defines how Nitro is served. Options include `Latest` (default), `Insider`, `Embedded`, and `Version(string version)`. | +| ServeMode | `ServeMode` | Defines which version of Nitro is served from the CDN: `Latest` (default), `Insider`, or `Version(string version)`. See `WithEmbeddedNitroAssets()`. | | Title | `string` | Specifies the title of the Nitro page. | | Document | `string` | Specifies the default document content. | | IncludeCookies | `bool?` | If `true`, includes cookies in the HTTP call to the GraphQL backend. | From a7f13c1dd36cc3e7a1b10606cb072336ac975bf6 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Mon, 19 Oct 2026 16:58:58 +0000 Subject: [PATCH 3/6] Shorten the SetNitroAppSlug summary --- .../AspNetCore.Pipeline/Extensions/HttpContextExtensions.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpContextExtensions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpContextExtensions.cs index 8837956bfdc..b93eeedd052 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpContextExtensions.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpContextExtensions.cs @@ -42,8 +42,7 @@ public static bool MayReachNitroApp(this HttpContext context, PathString path) } /// - /// Sets the Nitro app slug route value for a request below , for - /// hosts that mount the Nitro app without endpoint routing. + /// Sets the Nitro app slug route value from the request path below . /// public static void SetNitroAppSlug(this HttpContext context, PathString path) { From 38e95ef4a057b6c615eb29ad14546d123728bab3 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:47:59 +0000 Subject: [PATCH 4/6] Set the Nitro app path with NitroAppMiddleware.SetSlug ChilliCream.Nitro.App now only reads the slug route value on the endpoint the Nitro app is mounted on. Elsewhere, the host sets the path with NitroAppMiddleware.SetSlug, so MapGraphQL sets it whenever the request is below the GraphQL path, also when another endpoint matched. --- .../EndpointRouteBuilderExtensions.cs | 8 ++---- .../Extensions/HttpContextExtensions.cs | 5 ++-- .../AspNetCore.Tests/EmbeddedNitroAppTests.cs | 28 +++++++++++++++++++ 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/EndpointRouteBuilderExtensions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/EndpointRouteBuilderExtensions.cs index 746c4f96e8b..8ab9480321d 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/EndpointRouteBuilderExtensions.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/EndpointRouteBuilderExtensions.cs @@ -148,12 +148,8 @@ public static IApplicationBuilder MapGraphQL( branch => branch .Use(next => context => { - // Endpoint routing provides the slug; without it, derive it from the path. - if (context.GetEndpoint() is null) - { - context.SetNitroAppSlug(path); - } - + // Used when the Nitro app is not mounted on its own endpoint. + context.SetNitroAppSlug(path); return next(context); }) .UseNitroApp(serverOptions.Tool, EmbeddedNitroApp.AssetSource)) diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpContextExtensions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpContextExtensions.cs index b93eeedd052..4fad4745ba9 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpContextExtensions.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpContextExtensions.cs @@ -42,7 +42,7 @@ public static bool MayReachNitroApp(this HttpContext context, PathString path) } /// - /// Sets the Nitro app slug route value from the request path below . + /// Sets the path the Nitro app serves from the request path below . /// public static void SetNitroAppSlug(this HttpContext context, PathString path) { @@ -50,8 +50,7 @@ public static void SetNitroAppSlug(this HttpContext context, PathString path) if (request.Path.StartsWithSegments(path, StringComparison.OrdinalIgnoreCase, out var remaining)) { - request.RouteValues[NitroAppMiddleware.SlugRouteValueName] = - remaining.Value?.TrimStart('/') ?? string.Empty; + NitroAppMiddleware.SetSlug(context, remaining.Value?.TrimStart('/') ?? string.Empty); } } diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/EmbeddedNitroAppTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/EmbeddedNitroAppTests.cs index a6371a476c9..463f0a0d890 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/EmbeddedNitroAppTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/EmbeddedNitroAppTests.cs @@ -129,6 +129,34 @@ public async Task MapGroup_WithEmbeddedNitroAssets_Wins_Over_Schema_Wide_ServeMo Assert.Null(forwarder.DestinationPrefix); } + [Fact] + public async Task MapGraphQL_Middleware_Does_Not_Proxy_Nitro_For_Other_Endpoints_With_A_Slug() + { + // arrange + var forwarder = new RecordingForwarder(); + var server = ServerFactory.Create( + services => services + .AddSingleton(forwarder) + .AddRouting() + .AddGraphQLServer() + .AddStarWarsTypes() + .AddStarWarsRepositories(), + app => app + .UseRouting() + .MapGraphQL("/graphql", ISchemaDefinition.DefaultName) + .UseEndpoints(e => e.MapGet("/posts/{slug}", () => "post"))); + + // act + await GetHtmlAsync(server, "/posts/hello"); + var postDestination = forwarder.DestinationPrefix; + var nitro = await GetHtmlAsync(server, "/graphql/"); + + // assert + Assert.Null(postDestination); + Assert.Equal(HttpStatusCode.OK, nitro.StatusCode); + Assert.Equal("https://cdn.chillicream.com/web/latest/", forwarder.DestinationPrefix); + } + [Fact] public async Task MapGraphQL_With_ServeMode_Embedded_Still_Serves_Embedded_Nitro() { From f8d0225575bc162fb5c07e66a22f8ab0e724e356 Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Mon, 19 Oct 2026 18:42:45 +0000 Subject: [PATCH 5/6] Mount the Nitro app in endpoint or path mode The MapGraphQL and MapNitroApp endpoints use UseNitroApp, which serves the slug route value. Hosts without endpoint routing (Azure Functions and IApplicationBuilder.MapGraphQL used as middleware) use NitroAppMiddleware.Create(options, path). SetNitroAppSlug is gone. MayReachNitroApp compared the request path with the group-relative GraphQL path, so inside a MapGroup every request reached the Nitro app and GraphQL clients got Nitro's HTML or a redirect. On an endpoint, an empty slug now identifies the GraphQL endpoint path. --- .../EndpointRouteBuilderExtensions.cs | 31 ++++++++++++----- .../Extensions/HttpContextExtensions.cs | 21 +++++------- .../AspNetCore.Tests/EmbeddedNitroAppTests.cs | 34 ++++++++++++++++++- ...zureFunctionServiceCollectionExtensions.cs | 13 +++---- 4 files changed, 68 insertions(+), 31 deletions(-) diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/EndpointRouteBuilderExtensions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/EndpointRouteBuilderExtensions.cs index 8ab9480321d..6e12393ffd1 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/EndpointRouteBuilderExtensions.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/EndpointRouteBuilderExtensions.cs @@ -87,7 +87,7 @@ public static GraphQLEndpointConventionBuilder MapGraphQL( var pattern = Parse(path + "/{**slug}"); var requestPipeline = endpointRouteBuilder.CreateApplicationBuilder(); var services = endpointRouteBuilder.ServiceProvider; - requestPipeline.MapGraphQL(path, schemaNameOrDefault); + MapGraphQLPipeline(requestPipeline, path, schemaNameOrDefault, mountedOnEndpoint: true); var serverOptions = services.GetServerOptions(schemaNameOrDefault); return new GraphQLEndpointConventionBuilder( @@ -124,6 +124,15 @@ public static IApplicationBuilder MapGraphQL( ArgumentNullException.ThrowIfNull(applicationBuilder); ArgumentException.ThrowIfNullOrEmpty(schemaName); + return MapGraphQLPipeline(applicationBuilder, path, schemaName, mountedOnEndpoint: false); + } + + private static IApplicationBuilder MapGraphQLPipeline( + IApplicationBuilder applicationBuilder, + PathString path, + string schemaName, + bool mountedOnEndpoint) + { path = path.ToString().TrimEnd('/'); var services = applicationBuilder.ApplicationServices; @@ -145,14 +154,20 @@ public static IApplicationBuilder MapGraphQL( executor, serverOptions, path, MiddlewareRoutingType.Integrated)) .UseWhen( context => context.MayReachNitroApp(path), - branch => branch - .Use(next => context => + branch => + { + if (mountedOnEndpoint) + { + branch.UseNitroApp(serverOptions.Tool, EmbeddedNitroApp.AssetSource); + } + else { - // Used when the Nitro app is not mounted on its own endpoint. - context.SetNitroAppSlug(path); - return next(context); - }) - .UseNitroApp(serverOptions.Tool, EmbeddedNitroApp.AssetSource)) + branch.Use(NitroAppMiddleware.Create( + serverOptions.Tool, + path, + embeddedAssetSource: EmbeddedNitroApp.AssetSource)); + } + }) .Use(MiddlewareFactory.CreateHttpUnsupportedRequestMiddleware( executor, serverOptions, diff --git a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpContextExtensions.cs b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpContextExtensions.cs index 4fad4745ba9..db345c6b9e1 100644 --- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpContextExtensions.cs +++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpContextExtensions.cs @@ -2,6 +2,7 @@ using HotChocolate.AspNetCore.Formatters; using HotChocolate.AspNetCore.Utilities; using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; namespace HotChocolate.AspNetCore; @@ -30,7 +31,7 @@ public static bool MayReachNitroApp(this HttpContext context, PathString path) { var request = context.Request; - if (!request.IsGetOrHeadMethod() || !request.IsEndpointPath(path)) + if (!request.IsGetOrHeadMethod() || !IsGraphQLEndpointPath(context, path)) { return true; } @@ -41,18 +42,12 @@ public static bool MayReachNitroApp(this HttpContext context, PathString path) && DefaultHttpResponseFormatter.PrefersHtml(acceptHeader.AcceptMediaTypes); } - /// - /// Sets the path the Nitro app serves from the request path below . - /// - public static void SetNitroAppSlug(this HttpContext context, PathString path) - { - var request = context.Request; - - if (request.Path.StartsWithSegments(path, StringComparison.OrdinalIgnoreCase, out var remaining)) - { - NitroAppMiddleware.SetSlug(context, remaining.Value?.TrimStart('/') ?? string.Empty); - } - } + // Inside a route group, path is relative to the group. On an endpoint, an empty slug route + // value identifies the endpoint path instead. + private static bool IsGraphQLEndpointPath(HttpContext context, PathString path) + => context.Request.IsEndpointPath(path) + || (context.GetEndpoint() is not null + && string.IsNullOrEmpty(context.GetRouteValue(NitroAppMiddleware.SlugRouteValueName)?.ToString())); public static string? TryGetCostSwitch(this HttpContext context) { diff --git a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/EmbeddedNitroAppTests.cs b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/EmbeddedNitroAppTests.cs index 463f0a0d890..90c104feded 100644 --- a/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/EmbeddedNitroAppTests.cs +++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/EmbeddedNitroAppTests.cs @@ -157,6 +157,35 @@ public async Task MapGraphQL_Middleware_Does_Not_Proxy_Nitro_For_Other_Endpoints Assert.Equal("https://cdn.chillicream.com/web/latest/", forwarder.DestinationPrefix); } + [Theory] + [InlineData("")] + [InlineData("/api")] + public async Task MapGraphQL_Does_Not_Serve_Nitro_To_GraphQL_Clients(string groupPrefix) + { + // arrange + var forwarder = new RecordingForwarder(); + var server = CreateServer( + forwarder, + e => + { + var target = groupPrefix.Length == 0 ? e : e.MapGroup(groupPrefix); + target.MapGraphQL(); + }); + + // act + var withSlash = await GetAsync(server, groupPrefix + "/graphql/", "application/graphql-response+json"); + var withoutSlash = await GetAsync(server, groupPrefix + "/graphql", "application/graphql-response+json"); + var clientDestination = forwarder.DestinationPrefix; + var browser = await GetHtmlAsync(server, groupPrefix + "/graphql/"); + + // assert + Assert.Equal(HttpStatusCode.NotFound, withSlash.StatusCode); + Assert.Equal(HttpStatusCode.NotFound, withoutSlash.StatusCode); + Assert.Null(clientDestination); + Assert.Equal(HttpStatusCode.OK, browser.StatusCode); + Assert.Equal("https://cdn.chillicream.com/web/latest/", forwarder.DestinationPrefix); + } + [Fact] public async Task MapGraphQL_With_ServeMode_Embedded_Still_Serves_Embedded_Nitro() { @@ -222,9 +251,12 @@ private TestServer CreateServer(RecordingForwarder forwarder, Action GetHtmlAsync(TestServer server, string path) + => GetAsync(server, path, "text/html"); + + private static Task GetAsync(TestServer server, string path, string accept) { var request = new HttpRequestMessage(HttpMethod.Get, path); - request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("text/html")); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(accept)); return server.CreateClient().SendAsync(request, TestContext.Current.CancellationToken); } diff --git a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs index 374e4e13e61..cb20f8cde6b 100644 --- a/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs +++ b/src/HotChocolate/AzureFunctions/src/HotChocolate.AzureFunctions/Extensions/HotChocolateAzureFunctionServiceCollectionExtensions.cs @@ -104,15 +104,10 @@ private static IServiceCollection AddAzureFunctionsGraphQLRequestExecutor( .Use(MiddlewareFactory.CreateHttpGetSchemaMiddleware(executor, serverOptions, path, MiddlewareRoutingType.Integrated)) .UseWhen( context => context.MayReachNitroApp(path), - branch => branch - .Use(next => context => - { - context.SetNitroAppSlug(path); - return next(context); - }) - .Use(NitroAppMiddleware.Create( - serverOptions.Tool, - embeddedAssetSource: EmbeddedNitroApp.AssetSource))) + branch => branch.Use(NitroAppMiddleware.Create( + serverOptions.Tool, + path, + embeddedAssetSource: EmbeddedNitroApp.AssetSource))) .Use(MiddlewareFactory.CreateHttpUnsupportedRequestMiddleware( executor, serverOptions, From 7ab82a849e9017f751c2d986ca30cd7cfe6f83ce Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Mon, 19 Oct 2026 20:04:10 +0000 Subject: [PATCH 6/6] Test the Nitro app in Azure Functions Cover the Nitro app root and config file in both the in-process and the isolated model, and that Tool.Enable = false turns the Nitro app off. --- .../Helpers/TestHttpRequestDataHelper.cs | 3 +- .../IsolatedProcessEndToEndTests.cs | 39 +++++++++++++++++ .../Helpers/TestHttpContextHelper.cs | 5 +-- .../InProcessEndToEndTests.cs | 43 +++++++++++++++++++ 4 files changed, 85 insertions(+), 5 deletions(-) diff --git a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/Helpers/TestHttpRequestDataHelper.cs b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/Helpers/TestHttpRequestDataHelper.cs index a1dd32d736e..a181f90cb6e 100644 --- a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/Helpers/TestHttpRequestDataHelper.cs +++ b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/Helpers/TestHttpRequestDataHelper.cs @@ -2,7 +2,6 @@ using Microsoft.Azure.Functions.Worker.Http; using static HotChocolate.AzureFunctions.Tests.Helpers.TestHttpContextHelper; using static Microsoft.Net.Http.Headers.HeaderNames; -using IO = System.IO; namespace HotChocolate.AzureFunctions.IsolatedProcess.Tests.Helpers; @@ -31,7 +30,7 @@ public static HttpRequestData NewNitroHttpRequestData( HttpRequestData httpRequestData = new MockHttpRequestData( new MockFunctionContext(serviceProvider), HttpMethods.Get, - new Uri(IO.Path.Combine(DefaultAzFuncGraphQLUri.ToString(), path))); + new Uri(DefaultAzFuncGraphQLUri.ToString().TrimEnd('/') + "/" + path)); //Ensure we accept Text/Html for Nitro requests... httpRequestData.Headers.Add(Accept, TestConstants.DefaultNitroContentType); 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 c96bfabda27..d4165de9a3a 100644 --- a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/IsolatedProcessEndToEndTests.cs +++ b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.IsolatedProcess.Tests/IsolatedProcessEndToEndTests.cs @@ -235,6 +235,45 @@ public async Task AzFuncIsolatedProcess_NitroTestAsync() Assert.True(resultContent.Contains("")); } + [Theory] + [InlineData("", " b.AddQueryType( + d => d.Name("Query").Field("person").Resolve("Luke Skywalker"))) + .Build(); + + var requestExecutor = host.Services.GetRequiredService(); + var httpRequestData = TestHttpRequestDataHelper.NewNitroHttpRequestData(host.Services, path); + + var httpResponseData = await requestExecutor.ExecuteAsync(httpRequestData); + + var resultContent = await ReadResponseAsStringAsync(httpResponseData); + Assert.Equal(HttpStatusCode.OK, httpResponseData.StatusCode); + Assert.Contains(expectedContent, resultContent); + } + + [Fact] + public async Task AzFuncIsolatedProcess_NitroDisabledTestAsync() + { + var host = new MockIsolatedProcessHostBuilder() + .AddGraphQLFunction( + b => b + .AddQueryType(d => d.Name("Query").Field("person").Resolve("Luke Skywalker")) + .ModifyFunctionOptions(o => o.Tool.Enable = false)) + .Build(); + + var requestExecutor = host.Services.GetRequiredService(); + var httpRequestData = TestHttpRequestDataHelper.NewNitroHttpRequestData(host.Services, "index.html"); + + var httpResponseData = await requestExecutor.ExecuteAsync(httpRequestData); + + Assert.Equal(HttpStatusCode.NotFound, httpResponseData.StatusCode); + } + [Theory] [InlineData(HttpTransportVersion.Legacy, HttpStatusCode.NotFound, null)] [InlineData(HttpTransportVersion.Draft20250508, HttpStatusCode.NotFound, null)] diff --git a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/Helpers/TestHttpContextHelper.cs b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/Helpers/TestHttpContextHelper.cs index 4bf82f03322..8dfc7e63ba8 100644 --- a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/Helpers/TestHttpContextHelper.cs +++ b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/Helpers/TestHttpContextHelper.cs @@ -3,7 +3,6 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Microsoft.Net.Http.Headers; -using IO = System.IO; namespace HotChocolate.AzureFunctions.Tests.Helpers; @@ -31,9 +30,9 @@ public static HttpContext NewGraphQLHttpContext(string query) return httpContext; } - public static HttpContext NewNitroHttpContext() + public static HttpContext NewNitroHttpContext(string path = "index.html") { - var uri = new Uri(IO.Path.Combine(DefaultAzFuncGraphQLUri.ToString(), "index.html")); + var uri = new Uri(DefaultAzFuncGraphQLUri.ToString().TrimEnd('/') + "/" + path); var httpContext = new DefaultHttpContext(); diff --git a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/InProcessEndToEndTests.cs b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/InProcessEndToEndTests.cs index c5ec61e4e01..a60ba4f550c 100644 --- a/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/InProcessEndToEndTests.cs +++ b/src/HotChocolate/AzureFunctions/test/HotChocolate.AzureFunctions.Tests/InProcessEndToEndTests.cs @@ -181,6 +181,49 @@ public async Task AzFuncInProcess_NitroTestAsync() Assert.True(resultContent.Contains("")); } + [Theory] + [InlineData("", " d.Name("Query").Field("person").Resolve("Luke Skywalker")); + + var requestExecutor = hostBuilder.BuildServiceProvider().GetRequiredService(); + var httpContext = TestHttpContextHelper.NewNitroHttpContext(path); + + await requestExecutor.ExecuteAsync(httpContext.Request); + + var resultContent = await httpContext.ReadResponseContentAsync(); + Assert.Equal(StatusCodes.Status200OK, httpContext.Response.StatusCode); + Assert.Contains(expectedContent, resultContent); + } + + [Fact] + public async Task AzFuncInProcess_NitroDisabledTestAsync() + { + var hostBuilder = new MockInProcessFunctionsHostBuilder(); + + hostBuilder.Services.AddHttpContextAccessor(); + + hostBuilder + .AddGraphQLFunction() + .AddQueryType(d => d.Name("Query").Field("person").Resolve("Luke Skywalker")) + .ModifyFunctionOptions(o => o.Tool.Enable = false); + + var requestExecutor = hostBuilder.BuildServiceProvider().GetRequiredService(); + var httpContext = TestHttpContextHelper.NewNitroHttpContext(); + + await requestExecutor.ExecuteAsync(httpContext.Request); + + Assert.Equal(StatusCodes.Status404NotFound, httpContext.Response.StatusCode); + } + [Theory] [InlineData(HttpTransportVersion.Legacy, StatusCodes.Status404NotFound, null)] [InlineData(HttpTransportVersion.Draft20250508, StatusCodes.Status404NotFound, null)]