diff --git a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/Http.ts b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/Http.ts
index d914b6b22..a0a1cd5f4 100644
--- a/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/Http.ts
+++ b/src/Microsoft.AspNetCore.Blazor.Browser.JS/src/Services/Http.ts
@@ -15,7 +15,7 @@ async function sendAsync(id: number, method: string, requestUri: string, body: s
let response: Response;
let responseText: string;
- const requestInit = fetchArgs || {};
+ const requestInit: RequestInit = fetchArgs || {};
requestInit.method = method;
requestInit.body = body || undefined;
diff --git a/src/Microsoft.AspNetCore.Blazor.Browser/Http/BrowserHttpMessageHandler.cs b/src/Microsoft.AspNetCore.Blazor.Browser/Http/BrowserHttpMessageHandler.cs
index 4bf5dc6c4..beedb93db 100644
--- a/src/Microsoft.AspNetCore.Blazor.Browser/Http/BrowserHttpMessageHandler.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Browser/Http/BrowserHttpMessageHandler.cs
@@ -17,6 +17,13 @@ namespace Microsoft.AspNetCore.Blazor.Browser.Http
///
public class BrowserHttpMessageHandler : HttpMessageHandler
{
+ ///
+ /// Gets or sets the default value of the 'credentials' option on outbound HTTP requests.
+ /// Defaults to .
+ ///
+ public static FetchCredentialsOption DefaultCredentials { get; set; }
+ = FetchCredentialsOption.SameOrigin;
+
static object _idLock = new object();
static int _nextRequestId = 0;
static IDictionary> _pendingRequests
@@ -47,7 +54,7 @@ protected override async Task SendAsync(
request.RequestUri,
request.Content == null ? null : await GetContentAsString(request.Content),
SerializeHeadersAsJson(request),
- fetchArgs);
+ fetchArgs ?? CreateDefaultFetchArgs());
return await tcs.Task;
}
@@ -93,6 +100,26 @@ private static void ReceiveResponse(
}
}
+ private static object CreateDefaultFetchArgs()
+ => new { credentials = GetDefaultCredentialsString() };
+
+ private static object GetDefaultCredentialsString()
+ {
+ // See https://developer.mozilla.org/en-US/docs/Web/API/Request/credentials for
+ // standard values and meanings
+ switch (DefaultCredentials)
+ {
+ case FetchCredentialsOption.Omit:
+ return "omit";
+ case FetchCredentialsOption.SameOrigin:
+ return "same-origin";
+ case FetchCredentialsOption.Include:
+ return "include";
+ default:
+ throw new ArgumentException($"Unknown credentials option '{DefaultCredentials}'.");
+ }
+ }
+
// Keep in sync with TypeScript class in Http.ts
private class ResponseDescriptor
{
diff --git a/src/Microsoft.AspNetCore.Blazor.Browser/Http/FetchCredentialsOption.cs b/src/Microsoft.AspNetCore.Blazor.Browser/Http/FetchCredentialsOption.cs
new file mode 100644
index 000000000..1a5d95853
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Blazor.Browser/Http/FetchCredentialsOption.cs
@@ -0,0 +1,28 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+namespace Microsoft.AspNetCore.Blazor.Browser.Http
+{
+ ///
+ /// Specifies a value for the 'credentials' option on outbound HTTP requests.
+ ///
+ public enum FetchCredentialsOption
+ {
+ ///
+ /// Advises the browser never to send credentials (such as cookies or HTTP auth headers).
+ ///
+ Omit,
+
+ ///
+ /// Advises the browser to send credentials (such as cookies or HTTP auth headers)
+ /// only if the target URL is on the same origin as the calling application.
+ ///
+ SameOrigin,
+
+ ///
+ /// Advises the browser to send credentials (such as cookies or HTTP auth headers)
+ /// even for cross-origin requests.
+ ///
+ Include,
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/BrowserFixture.cs b/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/BrowserFixture.cs
index 3cdd7ef79..3a3bc298a 100644
--- a/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/BrowserFixture.cs
+++ b/test/Microsoft.AspNetCore.Blazor.E2ETest/Infrastructure/BrowserFixture.cs
@@ -15,6 +15,8 @@ public class BrowserFixture : IDisposable
public BrowserFixture()
{
var opts = new ChromeOptions();
+
+ // Comment this out if you want to watch or interact with the browser (e.g., for debugging)
opts.AddArgument("--headless");
// On Windows/Linux, we don't need to set opts.BinaryLocation
diff --git a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/HttpClientTest.cs b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/HttpClientTest.cs
index 2b6ab8d33..b18bcf845 100644
--- a/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/HttpClientTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.E2ETest/Tests/HttpClientTest.cs
@@ -106,6 +106,38 @@ public void CanSetRequestReferer()
Assert.EndsWith("/test-referrer", _responseBody.Text);
}
+ [Fact]
+ public void CanSendAndReceiveCookies()
+ {
+ var app = MountTestComponent();
+ var deleteButton = app.FindElement(By.Id("delete"));
+ var incrementButton = app.FindElement(By.Id("increment"));
+ app.FindElement(By.TagName("input")).SendKeys(_apiServerFixture.RootUri.ToString());
+
+ // Ensure we're starting from a clean state
+ deleteButton.Click();
+ Assert.Equal("Reset completed", WaitAndGetResponseText());
+
+ // Observe that subsequent requests manage to preserve state via cookie
+ incrementButton.Click();
+ Assert.Equal("Counter value is 1", WaitAndGetResponseText());
+ incrementButton.Click();
+ Assert.Equal("Counter value is 2", WaitAndGetResponseText());
+
+ // Verify that attempting to delete a cookie actually works
+ deleteButton.Click();
+ Assert.Equal("Reset completed", WaitAndGetResponseText());
+ incrementButton.Click();
+ Assert.Equal("Counter value is 1", WaitAndGetResponseText());
+
+ string WaitAndGetResponseText()
+ {
+ new WebDriverWait(Browser, TimeSpan.FromSeconds(30)).Until(
+ driver => driver.FindElement(By.Id("response-text")) != null);
+ return app.FindElement(By.Id("response-text")).Text;
+ }
+ }
+
private void IssueRequest(string requestMethod, string relativeUri, string requestBody = null)
{
var targetUri = new Uri(_apiServerFixture.RootUri, relativeUri);
diff --git a/test/testapps/BasicTestApp/HttpClientTest/CookieCounterComponent.cshtml b/test/testapps/BasicTestApp/HttpClientTest/CookieCounterComponent.cshtml
new file mode 100644
index 000000000..9efde52e6
--- /dev/null
+++ b/test/testapps/BasicTestApp/HttpClientTest/CookieCounterComponent.cshtml
@@ -0,0 +1,38 @@
+@inject System.Net.Http.HttpClient Http
+
+
Cookie counter
+
The server increments the count by one on each request.
+
TestServer base URL:
+
+
+
+@if (!requestInProgress)
+{
+
@responseText
+}
+
+@functions
+{
+ bool requestInProgress = false;
+ string testServerBaseUrl;
+ string responseText;
+
+ async void DeleteCookie()
+ {
+ await DoRequest("api/cookie/reset");
+ StateHasChanged();
+ }
+
+ async void GetAndIncrementCounter()
+ {
+ await DoRequest("api/cookie/increment");
+ StateHasChanged();
+ }
+
+ async Task DoRequest(string url)
+ {
+ requestInProgress = true;
+ responseText = await Http.GetStringAsync(testServerBaseUrl + url);
+ requestInProgress = false;
+ }
+}
diff --git a/test/testapps/BasicTestApp/Program.cs b/test/testapps/BasicTestApp/Program.cs
index c8b87d496..39e5bc6d0 100644
--- a/test/testapps/BasicTestApp/Program.cs
+++ b/test/testapps/BasicTestApp/Program.cs
@@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+using Microsoft.AspNetCore.Blazor.Browser.Http;
using Microsoft.AspNetCore.Blazor.Browser.Interop;
using Microsoft.AspNetCore.Blazor.Browser.Rendering;
using Microsoft.AspNetCore.Blazor.Components;
@@ -12,6 +13,10 @@ public class Program
{
static void Main(string[] args)
{
+ // Needed because the test server runs on a different port than the client app,
+ // and we want to test sending/receiving cookies undering this config
+ BrowserHttpMessageHandler.DefaultCredentials = FetchCredentialsOption.Include;
+
// Signal to tests that we're ready
RegisteredFunction.Invoke