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 140b3e88c28..6e12393ffd1 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;
@@ -86,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(
@@ -123,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;
@@ -144,7 +154,20 @@ public static IApplicationBuilder MapGraphQL(
executor, serverOptions, path, MiddlewareRoutingType.Integrated))
.UseWhen(
context => context.MayReachNitroApp(path),
- branch => branch.UseNitroApp(path, serverOptions.Tool))
+ branch =>
+ {
+ if (mountedOnEndpoint)
+ {
+ branch.UseNitroApp(serverOptions.Tool, EmbeddedNitroApp.AssetSource);
+ }
+ else
+ {
+ branch.Use(NitroAppMiddleware.Create(
+ serverOptions.Tool,
+ path,
+ embeddedAssetSource: EmbeddedNitroApp.AssetSource));
+ }
+ })
.Use(MiddlewareFactory.CreateHttpUnsupportedRequestMiddleware(
executor,
serverOptions,
@@ -529,7 +552,7 @@ public static NitroAppEndpointConventionBuilder MapNitroApp(
var nitroOptions = new NitroAppOptions { GraphQLEndpoint = relativeRequestPath };
requestPipeline
- .UseNitroApp(toolPath, nitroOptions)
+ .UseNitroApp(nitroOptions, EmbeddedNitroApp.AssetSource)
.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..db345c6b9e1 100644
--- a/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpContextExtensions.cs
+++ b/src/HotChocolate/AspNetCore/src/AspNetCore.Pipeline/Extensions/HttpContextExtensions.cs
@@ -1,6 +1,8 @@
+using ChilliCream.Nitro.App;
using HotChocolate.AspNetCore.Formatters;
using HotChocolate.AspNetCore.Utilities;
using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Routing;
namespace HotChocolate.AspNetCore;
@@ -29,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;
}
@@ -40,6 +42,13 @@ public static bool MayReachNitroApp(this HttpContext context, PathString path)
&& DefaultHttpResponseFormatter.PrefersHtml(acceptHeader.AcceptMediaTypes);
}
+ // 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)
{
var headers = context.Request.Headers;
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..90c104feded
--- /dev/null
+++ b/src/HotChocolate/AspNetCore/test/AspNetCore.Tests/EmbeddedNitroAppTests.cs
@@ -0,0 +1,288 @@
+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_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);
+ }
+
+ [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()
+ {
+ // 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)
+ => 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(accept));
+ 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 7cb1fab0904..cb20f8cde6b 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;
@@ -8,7 +9,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 +104,10 @@ 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(NitroAppMiddleware.Create(
+ serverOptions.Tool,
+ path,
+ embeddedAssetSource: EmbeddedNitroApp.AssetSource)))
.Use(MiddlewareFactory.CreateHttpUnsupportedRequestMiddleware(
executor,
serverOptions,
@@ -139,46 +142,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);
- }
}
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/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)]
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. |