Skip to content

Commit 7d081d7

Browse files
authored
Add accept header to grpc-web-text requests (#1522)
1 parent 520c574 commit 7d081d7

File tree

7 files changed

+124
-21
lines changed

7 files changed

+124
-21
lines changed

src/Grpc.AspNetCore.Web/Internal/GrpcWebFeature.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ internal class GrpcWebFeature :
3434
private readonly IHttpResponseTrailersFeature? _initialTrailersFeature;
3535
private bool _isComplete;
3636

37-
public GrpcWebFeature(ServerGrpcWebMode grpcWebMode, HttpContext httpContext)
37+
public GrpcWebFeature(ServerGrpcWebContext grcpWebContext, HttpContext httpContext)
3838
{
3939
// Capture existing features. We'll use these internally, and restore them onto the context
4040
// once the middleware has finished executing.
@@ -52,14 +52,20 @@ public GrpcWebFeature(ServerGrpcWebMode grpcWebMode, HttpContext httpContext)
5252
var innerWriter = _initialResponseFeature.Writer ?? httpContext.Response.BodyWriter;
5353

5454
Trailers = new HeaderDictionary();
55-
if (grpcWebMode == ServerGrpcWebMode.GrpcWebText)
55+
if (grcpWebContext.Request == ServerGrpcWebMode.GrpcWebText)
5656
{
5757
Reader = new Base64PipeReader(innerReader);
58-
Writer = new Base64PipeWriter(innerWriter);
5958
}
6059
else
6160
{
6261
Reader = innerReader;
62+
}
63+
if (grcpWebContext.Response == ServerGrpcWebMode.GrpcWebText)
64+
{
65+
Writer = new Base64PipeWriter(innerWriter);
66+
}
67+
else
68+
{
6369
Writer = innerWriter;
6470
}
6571

src/Grpc.AspNetCore.Web/Internal/GrpcWebMiddleware.cs

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,15 +39,15 @@ public GrpcWebMiddleware(IOptions<GrpcWebOptions> options, ILogger<GrpcWebMiddle
3939

4040
public Task Invoke(HttpContext httpContext)
4141
{
42-
var mode = GetGrpcWebMode(httpContext);
43-
if (mode != ServerGrpcWebMode.None)
42+
var grcpWebContext = GetGrpcWebContext(httpContext);
43+
if (grcpWebContext.Request != ServerGrpcWebMode.None)
4444
{
4545
Log.DetectedGrpcWebRequest(_logger, httpContext.Request.ContentType!);
4646

4747
var metadata = httpContext.GetEndpoint()?.Metadata.GetMetadata<IGrpcWebEnabledMetadata>();
4848
if (metadata?.GrpcWebEnabled ?? _options.DefaultEnabled)
4949
{
50-
return HandleGrpcWebRequest(httpContext, mode);
50+
return HandleGrpcWebRequest(httpContext, grcpWebContext);
5151
}
5252

5353
Log.GrpcWebRequestNotProcessed(_logger);
@@ -56,9 +56,9 @@ public Task Invoke(HttpContext httpContext)
5656
return _next(httpContext);
5757
}
5858

59-
private async Task HandleGrpcWebRequest(HttpContext httpContext, ServerGrpcWebMode mode)
59+
private async Task HandleGrpcWebRequest(HttpContext httpContext, ServerGrpcWebContext grcpWebContext)
6060
{
61-
var feature = new GrpcWebFeature(mode, httpContext);
61+
var feature = new GrpcWebFeature(grcpWebContext, httpContext);
6262

6363
var initialProtocol = httpContext.Request.Protocol;
6464

@@ -75,7 +75,7 @@ private async Task HandleGrpcWebRequest(HttpContext httpContext, ServerGrpcWebMo
7575

7676
if (CommonGrpcProtocolHelpers.IsContentType(GrpcWebProtocolConstants.GrpcContentType, httpContext.Response.ContentType!))
7777
{
78-
var contentType = mode == ServerGrpcWebMode.GrpcWeb
78+
var contentType = grcpWebContext.Response == ServerGrpcWebMode.GrpcWeb
7979
? GrpcWebProtocolConstants.GrpcWebContentType
8080
: GrpcWebProtocolConstants.GrpcWebTextContentType;
8181
var responseContentType = ResolveContentType(contentType, httpContext.Response.ContentType);
@@ -111,21 +111,46 @@ private static string ResolveContentType(string newContentType, string originalC
111111
return newContentType;
112112
}
113113

114-
internal static ServerGrpcWebMode GetGrpcWebMode(HttpContext httpContext)
114+
internal static ServerGrpcWebContext GetGrpcWebContext(HttpContext httpContext)
115115
{
116-
if (HttpMethods.IsPost(httpContext.Request.Method))
116+
var serverContext = new ServerGrpcWebContext();
117+
118+
if (TryGetWebMode(httpContext.Request.ContentType, out var requestMode))
117119
{
118-
if (CommonGrpcProtocolHelpers.IsContentType(GrpcWebProtocolConstants.GrpcWebContentType, httpContext.Request.ContentType))
120+
serverContext.Request = requestMode;
121+
122+
if (TryGetWebMode(httpContext.Request.Headers["Accept"], out var responseMode))
119123
{
120-
return ServerGrpcWebMode.GrpcWeb;
124+
serverContext.Response = responseMode;
121125
}
122-
else if (CommonGrpcProtocolHelpers.IsContentType(GrpcWebProtocolConstants.GrpcWebTextContentType, httpContext.Request.ContentType))
126+
else
123127
{
124-
return ServerGrpcWebMode.GrpcWebText;
128+
// If there isn't a request 'accept' header then default to mode to 'application/grpc`.
129+
serverContext.Response = ServerGrpcWebMode.GrpcWeb;
125130
}
126131
}
127-
128-
return ServerGrpcWebMode.None;
132+
133+
return serverContext;
134+
}
135+
136+
private static bool TryGetWebMode(string? contentType, out ServerGrpcWebMode mode)
137+
{
138+
if (!string.IsNullOrEmpty(contentType))
139+
{
140+
if (CommonGrpcProtocolHelpers.IsContentType(GrpcWebProtocolConstants.GrpcWebContentType, contentType))
141+
{
142+
mode = ServerGrpcWebMode.GrpcWeb;
143+
return true;
144+
}
145+
else if (CommonGrpcProtocolHelpers.IsContentType(GrpcWebProtocolConstants.GrpcWebTextContentType, contentType))
146+
{
147+
mode = ServerGrpcWebMode.GrpcWebText;
148+
return true;
149+
}
150+
}
151+
152+
mode = ServerGrpcWebMode.None;
153+
return false;
129154
}
130155

131156
private static class Log

src/Grpc.AspNetCore.Web/Internal/ServerGrpcWebMode.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818

1919
namespace Grpc.AspNetCore.Web.Internal
2020
{
21+
internal record struct ServerGrpcWebContext(ServerGrpcWebMode Request, ServerGrpcWebMode Response);
22+
2123
internal enum ServerGrpcWebMode
2224
{
2325
None,

src/Grpc.Net.Client.Web/GrpcWebHandler.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,14 @@ private async Task<HttpResponseMessage> SendAsyncCore(HttpRequestMessage request
114114
{
115115
request.Content = new GrpcWebRequestContent(request.Content!, GrpcWebMode);
116116

117+
// https://github.com/grpc/grpc/blob/f8a5022a2629e0929eb30e0583af66f0c220791b/doc/PROTOCOL-WEB.md
118+
// The client library should indicate to the server via the "Accept" header that the response stream
119+
// needs to be text encoded e.g. when XHR is used or due to security policies with XHR.
120+
if (GrpcWebMode == GrpcWebMode.GrpcWebText)
121+
{
122+
request.Headers.TryAddWithoutValidation("Accept", GrpcWebProtocolConstants.GrpcWebTextContentType);
123+
}
124+
117125
if (OperatingSystem.IsBrowser)
118126
{
119127
FixBrowserUserAgent(request);

test/Grpc.AspNetCore.Server.Tests/Web/GrpcWebFeatureTests.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public void Ctor_DefaultHttpContext_FeaturesSet()
3333
var httpContext = new DefaultHttpContext();
3434

3535
// Act
36-
var feature = new GrpcWebFeature(ServerGrpcWebMode.GrpcWeb, httpContext);
36+
var feature = CreateFeature(httpContext);
3737

3838
// Assert
3939
Assert.AreEqual(feature, httpContext.Features.Get<IHttpResponseBodyFeature>());
@@ -48,7 +48,7 @@ public void DetachFromContext_InitialHttpContext_FeaturesReset()
4848
// Arrange
4949
var httpContext = new DefaultHttpContext();
5050
var responseBodyFeature = httpContext.Features.Get<IHttpResponseBodyFeature>();
51-
var feature = new GrpcWebFeature(ServerGrpcWebMode.GrpcWeb, httpContext);
51+
var feature = CreateFeature(httpContext);
5252

5353
// Act
5454
feature.DetachFromContext(httpContext);
@@ -59,5 +59,12 @@ public void DetachFromContext_InitialHttpContext_FeaturesReset()
5959
Assert.AreEqual(null, httpContext.Features.Get<IHttpResponseTrailersFeature>());
6060
Assert.AreEqual(null, httpContext.Features.Get<IHttpResetFeature>());
6161
}
62+
63+
private static GrpcWebFeature CreateFeature(DefaultHttpContext httpContext)
64+
{
65+
return new GrpcWebFeature(
66+
new ServerGrpcWebContext(ServerGrpcWebMode.GrpcWeb, ServerGrpcWebMode.GrpcWeb),
67+
httpContext);
68+
}
6269
}
6370
}

test/Grpc.AspNetCore.Server.Tests/Web/GrpcWebMiddlewareTests.cs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,38 @@ public void GetGrpcWebMode_ContentTypes_Matched(string contentType, string expec
6666
httpContext.Request.ContentType = contentType;
6767

6868
// Act
69-
var grpcWebMode = GrpcWebMiddleware.GetGrpcWebMode(httpContext);
69+
var grpcWebContext = GrpcWebMiddleware.GetGrpcWebContext(httpContext);
7070

7171
// Assert
72-
Assert.AreEqual(Enum.Parse<ServerGrpcWebMode>(expectedGrpcWebMode), grpcWebMode);
72+
Assert.AreEqual(Enum.Parse<ServerGrpcWebMode>(expectedGrpcWebMode), grpcWebContext.Request);
73+
}
74+
75+
[TestCase(GrpcWebProtocolConstants.GrpcWebContentType, null,
76+
nameof(ServerGrpcWebMode.GrpcWeb), nameof(ServerGrpcWebMode.GrpcWeb))]
77+
[TestCase(GrpcWebProtocolConstants.GrpcWebContentType, GrpcWebProtocolConstants.GrpcWebTextContentType,
78+
nameof(ServerGrpcWebMode.GrpcWeb), nameof(ServerGrpcWebMode.GrpcWebText))]
79+
[TestCase(GrpcWebProtocolConstants.GrpcWebTextContentType, GrpcWebProtocolConstants.GrpcWebTextContentType,
80+
nameof(ServerGrpcWebMode.GrpcWebText), nameof(ServerGrpcWebMode.GrpcWebText))]
81+
[TestCase("application/json", null,
82+
nameof(ServerGrpcWebMode.None), nameof(ServerGrpcWebMode.None))]
83+
[TestCase("", null,
84+
nameof(ServerGrpcWebMode.None), nameof(ServerGrpcWebMode.None))]
85+
public void GetGrpcWebMode_Accept_Matched(
86+
string? contentType, string? accept,
87+
string expectedRequestGrpcWebMode, string expectedResponseGrpcWebMode)
88+
{
89+
// Arrange
90+
var httpContext = new DefaultHttpContext();
91+
httpContext.Request.Method = HttpMethods.Post;
92+
httpContext.Request.ContentType = contentType!;
93+
httpContext.Request.Headers["Accept"] = accept;
94+
95+
// Act
96+
var grpcWebContext = GrpcWebMiddleware.GetGrpcWebContext(httpContext);
97+
98+
// Assert
99+
Assert.AreEqual(Enum.Parse<ServerGrpcWebMode>(expectedRequestGrpcWebMode), grpcWebContext.Request);
100+
Assert.AreEqual(Enum.Parse<ServerGrpcWebMode>(expectedResponseGrpcWebMode), grpcWebContext.Response);
73101
}
74102

75103
[Test]

test/Grpc.Net.Client.Web.Tests/GrpcWebHandlerTests.cs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,33 @@ public async Task HttpVersion_Set_HttpRequestMessageVersionChanged()
8282
Assert.AreEqual(GrpcWebProtocolConstants.Http2Version, response.Version);
8383
}
8484

85+
[Test]
86+
public async Task GrpcWebMode_GrpcWebText_AcceptHeaderAdded()
87+
{
88+
// Arrange
89+
var request = new HttpRequestMessage
90+
{
91+
Version = GrpcWebProtocolConstants.Http2Version,
92+
Content = new ByteArrayContent(Array.Empty<byte>())
93+
{
94+
Headers = { ContentType = new MediaTypeHeaderValue("application/grpc") }
95+
}
96+
};
97+
var testHttpHandler = new TestHttpHandler();
98+
var grpcWebHandler = new GrpcWebHandler(GrpcWebMode.GrpcWebText)
99+
{
100+
InnerHandler = testHttpHandler
101+
};
102+
var messageInvoker = new HttpMessageInvoker(grpcWebHandler);
103+
104+
// Act
105+
await messageInvoker.SendAsync(request, CancellationToken.None);
106+
107+
// Assert
108+
Assert.IsTrue(testHttpHandler.Request!.Headers.TryGetValues("Accept", out var values));
109+
Assert.AreEqual(GrpcWebProtocolConstants.GrpcWebTextContentType, values!.Single());
110+
}
111+
85112
[Test]
86113
public async Task SendAsync_GrpcCall_ResponseStreamingPropertySet()
87114
{

0 commit comments

Comments
 (0)