diff --git a/src/RestSharp/KnownHeaders.cs b/src/RestSharp/KnownHeaders.cs
index f4c20645b..7943f9eed 100644
--- a/src/RestSharp/KnownHeaders.cs
+++ b/src/RestSharp/KnownHeaders.cs
@@ -30,6 +30,7 @@ public static class KnownHeaders {
public const string ContentLocation = "Content-Location";
public const string ContentRange = "Content-Range";
public const string ContentType = "Content-Type";
+ public const string Cookie = "Cookie";
public const string LastModified = "Last-Modified";
public const string ContentMD5 = "Content-MD5";
public const string Host = "Host";
diff --git a/src/RestSharp/Request/RequestHeaders.cs b/src/RestSharp/Request/RequestHeaders.cs
index 9d53ee4d7..d15b721b4 100644
--- a/src/RestSharp/Request/RequestHeaders.cs
+++ b/src/RestSharp/Request/RequestHeaders.cs
@@ -14,7 +14,10 @@
//
// ReSharper disable InvertIf
-namespace RestSharp;
+
+using System.Net;
+
+namespace RestSharp;
class RequestHeaders {
public ParametersCollection Parameters { get; } = new();
@@ -33,4 +36,13 @@ public RequestHeaders AddAcceptHeader(string[] acceptedContentTypes) {
return this;
}
+
+ // Add Cookie header from the cookie container
+ public RequestHeaders AddCookieHeaders(CookieContainer cookieContainer, Uri uri) {
+ var cookies = cookieContainer.GetCookieHeader(uri);
+ if (cookies.Length > 0) {
+ Parameters.AddParameter(new HeaderParameter(KnownHeaders.Cookie, cookies));
+ }
+ return this;
+ }
}
\ No newline at end of file
diff --git a/src/RestSharp/Request/RestRequest.cs b/src/RestSharp/Request/RestRequest.cs
index 3a13dd49c..4d81ac9ba 100644
--- a/src/RestSharp/Request/RestRequest.cs
+++ b/src/RestSharp/Request/RestRequest.cs
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+using System.Net;
using RestSharp.Extensions;
// ReSharper disable UnusedAutoPropertyAccessor.Global
@@ -29,6 +30,11 @@ public class RestRequest {
///
public RestRequest() => Method = Method.Get;
+ ///
+ /// Constructor for a rest request to a relative resource URL and optional method
+ ///
+ /// Resource to use
+ /// Method to use (defaults to Method.Get>
public RestRequest(string? resource, Method method = Method.Get) : this() {
Resource = resource ?? "";
Method = method;
@@ -58,6 +64,11 @@ static IEnumerable> ParseQuery(string query)
);
}
+ ///
+ /// Constructor for a rest request to a specific resource Uri and optional method
+ ///
+ /// Resource Uri to use
+ /// Method to use (defaults to Method.Get>
public RestRequest(Uri resource, Method method = Method.Get)
: this(resource.IsAbsoluteUri ? resource.AbsoluteUri : resource.OriginalString, method) { }
@@ -83,6 +94,11 @@ public RestRequest(Uri resource, Method method = Method.Get)
///
public ParametersCollection Parameters { get; } = new();
+ ///
+ /// Optional cookie container to use for the request. If not set, cookies are not passed.
+ ///
+ public CookieContainer? CookieContainer { get; set; }
+
///
/// Container of all the files to be uploaded with the request.
///
diff --git a/src/RestSharp/Request/RestRequestExtensions.cs b/src/RestSharp/Request/RestRequestExtensions.cs
index 80bfa5c13..a35da5621 100644
--- a/src/RestSharp/Request/RestRequestExtensions.cs
+++ b/src/RestSharp/Request/RestRequestExtensions.cs
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+using System.Net;
using System.Text.RegularExpressions;
using RestSharp.Extensions;
using RestSharp.Serializers;
@@ -445,6 +446,21 @@ public static RestRequest AddObject(this RestRequest request, T obj, params s
return request;
}
+ ///
+ /// Adds cookie to the cookie container.
+ ///
+ /// RestRequest to add the cookies to
+ /// Cookie name
+ /// Cookie value
+ /// Cookie path
+ /// Cookie domain, must not be an empty string
+ ///
+ public static RestRequest AddCookie(this RestRequest request, string name, string value, string path, string domain) {
+ request.CookieContainer ??= new CookieContainer();
+ request.CookieContainer.Add(new Cookie(name, value, path, domain));
+ return request;
+ }
+
static void CheckAndThrowsForInvalidHost(string name, string value) {
static bool InvalidHost(string host) => Uri.CheckHostName(PortSplitRegex.Split(host)[0]) == UriHostNameType.Unknown;
diff --git a/src/RestSharp/Response/RestResponse.cs b/src/RestSharp/Response/RestResponse.cs
index a9225ec23..cc7843b0b 100644
--- a/src/RestSharp/Response/RestResponse.cs
+++ b/src/RestSharp/Response/RestResponse.cs
@@ -64,7 +64,7 @@ internal static async Task FromHttpResponse(
HttpResponseMessage httpResponse,
RestRequest request,
Encoding encoding,
- CookieCollection cookieCollection,
+ CookieCollection? cookieCollection,
CalculateResponseStatus calculateResponseStatus,
CancellationToken cancellationToken
) {
diff --git a/src/RestSharp/RestClient.Async.cs b/src/RestSharp/RestClient.Async.cs
index 0a57d189f..f317ed354 100644
--- a/src/RestSharp/RestClient.Async.cs
+++ b/src/RestSharp/RestClient.Async.cs
@@ -12,6 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+using System.Net;
using RestSharp.Extensions;
namespace RestSharp;
@@ -32,7 +33,7 @@ public async Task ExecuteAsync(RestRequest request, CancellationTo
internalResponse.ResponseMessage!,
request,
Options.Encoding,
- CookieContainer.GetCookies(internalResponse.Url),
+ request.CookieContainer!.GetCookies(internalResponse.Url),
CalculateResponseStatus,
cancellationToken
)
@@ -64,16 +65,26 @@ async Task ExecuteInternal(RestRequest request, CancellationTo
var ct = cts.Token;
try {
+ // Make sure we have a cookie container if not provided in the request
+ var cookieContainer = request.CookieContainer ??= new CookieContainer();
var headers = new RequestHeaders()
.AddHeaders(request.Parameters)
.AddHeaders(DefaultParameters)
- .AddAcceptHeader(AcceptedContentTypes);
+ .AddAcceptHeader(AcceptedContentTypes)
+ .AddCookieHeaders(cookieContainer, url);
message.AddHeaders(headers);
if (request.OnBeforeRequest != null) await request.OnBeforeRequest(message).ConfigureAwait(false);
var responseMessage = await HttpClient.SendAsync(message, request.CompletionOption, ct).ConfigureAwait(false);
+ // Parse all the cookies from the response and update the cookie jar with cookies
+ if (responseMessage.Headers.TryGetValues("Set-Cookie", out var cookiesHeader)) {
+ foreach (var header in cookiesHeader) {
+ cookieContainer.SetCookies(url, header);
+ }
+ }
+
if (request.OnAfterRequest != null) await request.OnAfterRequest(responseMessage).ConfigureAwait(false);
return new InternalResponse(responseMessage, url, null, timeoutCts.Token);
diff --git a/src/RestSharp/RestClient.cs b/src/RestSharp/RestClient.cs
index b12b00d72..5abdba0d1 100644
--- a/src/RestSharp/RestClient.cs
+++ b/src/RestSharp/RestClient.cs
@@ -27,8 +27,6 @@ namespace RestSharp;
/// Client to translate RestRequests into Http requests and process response result
///
public partial class RestClient : IDisposable {
- public CookieContainer CookieContainer { get; }
-
///
/// Content types that will be sent in the Accept header. The list is populated from the known serializers.
/// If you need to send something else by default, set this property to a different value.
@@ -51,7 +49,6 @@ public RestClient(RestClientOptions options, Action? configu
UseDefaultSerializers();
Options = options;
- CookieContainer = Options.CookieContainer ?? new CookieContainer();
_disposeHttpClient = true;
var handler = new HttpClientHandler();
@@ -71,23 +68,27 @@ public RestClient() : this(new RestClientOptions()) { }
///
///
- /// Sets the BaseUrl property for requests made by this client instance
+ /// Creates an instance of RestClient using a specific BaseUrl for requests made by this client instance
///
- ///
+ /// Base URI for the new client
public RestClient(Uri baseUrl) : this(new RestClientOptions { BaseUrl = baseUrl }) { }
///
///
- /// Sets the BaseUrl property for requests made by this client instance
+ /// Creates an instance of RestClient using a specific BaseUrl for requests made by this client instance
///
- ///
+ /// Base URI for this new client as a string
public RestClient(string baseUrl) : this(new Uri(Ensure.NotEmptyString(baseUrl, nameof(baseUrl)))) { }
+ ///
+ /// Creates an instance of RestClient using a shared HttpClient and does not allocate one internally.
+ ///
+ /// HttpClient to use
+ /// True to dispose of the client, false to assume the caller does (defaults to false)
public RestClient(HttpClient httpClient, bool disposeHttpClient = false) {
UseDefaultSerializers();
HttpClient = httpClient;
- CookieContainer = new CookieContainer();
Options = new RestClientOptions();
_disposeHttpClient = disposeHttpClient;
@@ -96,15 +97,16 @@ public RestClient(HttpClient httpClient, bool disposeHttpClient = false) {
}
}
+ ///
+ /// Creates an instance of RestClient using a shared HttpClient and specific RestClientOptions and does not allocate one internally.
+ ///
+ /// HttpClient to use
+ /// RestClient options to use
+ /// True to dispose of the client, false to assume the caller does (defaults to false)
public RestClient(HttpClient httpClient, RestClientOptions options, bool disposeHttpClient = false) {
- if (options.CookieContainer != null) {
- throw new ArgumentException("Custom cookie container cannot be added to the HttpClient instance", nameof(options.CookieContainer));
- }
-
UseDefaultSerializers();
HttpClient = httpClient;
- CookieContainer = new CookieContainer();
Options = options;
_disposeHttpClient = disposeHttpClient;
@@ -134,9 +136,9 @@ void ConfigureHttpClient(HttpClient httpClient) {
}
void ConfigureHttpMessageHandler(HttpClientHandler handler) {
+ handler.UseCookies = false;
handler.Credentials = Options.Credentials;
handler.UseDefaultCredentials = Options.UseDefaultCredentials;
- handler.CookieContainer = CookieContainer;
handler.AutomaticDecompression = Options.AutomaticDecompression;
handler.PreAuthenticate = Options.PreAuthenticate;
handler.AllowAutoRedirect = Options.FollowRedirects;
diff --git a/src/RestSharp/RestClientExtensions.Config.cs b/src/RestSharp/RestClientExtensions.Config.cs
index 205812923..8868f7736 100644
--- a/src/RestSharp/RestClientExtensions.Config.cs
+++ b/src/RestSharp/RestClientExtensions.Config.cs
@@ -13,7 +13,6 @@
// limitations under the License.
//
-using System.Net;
using System.Text;
using RestSharp.Authenticators;
using RestSharp.Extensions;
@@ -43,23 +42,6 @@ public static partial class RestClientExtensions {
public static RestClient UseQueryEncoder(this RestClient client, Func queryEncoder)
=> client.With(x => x.EncodeQuery = queryEncoder);
- ///
- /// Adds cookie to the cookie container.
- ///
- ///
- /// Cookie name
- /// Cookie value
- /// Cookie path
- /// Cookie domain, must not be an empty string
- ///
- public static RestClient AddCookie(this RestClient client, string name, string value, string path, string domain) {
- lock (client.CookieContainer) {
- client.CookieContainer.Add(new Cookie(name, value, path, domain));
- }
-
- return client;
- }
-
public static RestClient UseAuthenticator(this RestClient client, IAuthenticator authenticator)
=> client.With(x => x.Authenticator = authenticator);
}
\ No newline at end of file
diff --git a/src/RestSharp/RestClientOptions.cs b/src/RestSharp/RestClientOptions.cs
index 43fec3f3a..83d820660 100644
--- a/src/RestSharp/RestClientOptions.cs
+++ b/src/RestSharp/RestClientOptions.cs
@@ -74,7 +74,6 @@ public RestClientOptions(string baseUrl) : this(new Uri(Ensure.NotEmptyString(ba
public CacheControlHeaderValue? CachePolicy { get; set; }
public bool FollowRedirects { get; set; } = true;
public bool? Expect100Continue { get; set; } = null;
- public CookieContainer? CookieContainer { get; set; }
public string? UserAgent { get; set; } = DefaultUserAgent;
///
diff --git a/test/RestSharp.Tests.Integrated/RequestTests.cs b/test/RestSharp.Tests.Integrated/RequestTests.cs
index 9eac238a0..b1fd86041 100644
--- a/test/RestSharp.Tests.Integrated/RequestTests.cs
+++ b/test/RestSharp.Tests.Integrated/RequestTests.cs
@@ -51,6 +51,63 @@ public async Task Can_Perform_GET_Async() {
response.Content.Should().Be(val);
}
+ [Fact]
+ public async Task Can_Perform_GET_Async_With_Request_Cookies() {
+ var request = new RestRequest("get-cookies") {
+ CookieContainer = new CookieContainer()
+ };
+ request.CookieContainer.Add(new Cookie("cookie", "value", null, _client.Options.BaseUrl.Host));
+ request.CookieContainer.Add(new Cookie("cookie2", "value2", null, _client.Options.BaseUrl.Host));
+ var response = await _client.ExecuteAsync(request);
+ response.Content.Should().Be("[\"cookie=value\",\"cookie2=value2\"]");
+ }
+
+ [Fact]
+ public async Task Can_Perform_GET_Async_With_Response_Cookies() {
+ var request = new RestRequest("set-cookies");
+ var response = await _client.ExecuteAsync(request);
+ response.Content.Should().Be("success");
+
+ // Check we got all our cookies
+ var domain = _client.Options.BaseUrl.Host;
+ var cookie = response.Cookies!.First(p => p.Name == "cookie1");
+ Assert.Equal("value1", cookie.Value);
+ Assert.Equal("/", cookie.Path);
+ Assert.Equal(domain, cookie.Domain);
+ Assert.Equal(DateTime.MinValue, cookie.Expires);
+ Assert.False(cookie.HttpOnly);
+
+ // Cookie 2 should vanish as the path will not match
+ cookie = response.Cookies!.FirstOrDefault(p => p.Name == "cookie2");
+ Assert.Null(cookie);
+
+ // Check cookie3 has a valid expiration
+ cookie = response.Cookies!.First(p => p.Name == "cookie3");
+ Assert.Equal("value3", cookie.Value);
+ Assert.Equal("/", cookie.Path);
+ Assert.Equal(domain, cookie.Domain);
+ Assert.True(cookie.Expires > DateTime.Now);
+
+ // Check cookie4 has a valid expiration
+ cookie = response.Cookies!.First(p => p.Name == "cookie4");
+ Assert.Equal("value4", cookie.Value);
+ Assert.Equal("/", cookie.Path);
+ Assert.Equal(domain, cookie.Domain);
+ Assert.True(cookie.Expires > DateTime.Now);
+
+ // Cookie 5 should vanish as the request is not SSL
+ cookie = response.Cookies!.FirstOrDefault(p => p.Name == "cookie5");
+ Assert.Null(cookie);
+
+ // Check cookie6 should be http only
+ cookie = response.Cookies!.First(p => p.Name == "cookie6");
+ Assert.Equal("value6", cookie.Value);
+ Assert.Equal("/", cookie.Path);
+ Assert.Equal(domain, cookie.Domain);
+ Assert.Equal(DateTime.MinValue, cookie.Expires);
+ Assert.True(cookie.HttpOnly);
+ }
+
[Fact]
public async Task Can_Timeout_GET_Async() {
var request = new RestRequest("timeout").AddBody("Body_Content");
diff --git a/test/RestSharp.Tests.Integrated/Server/TestServer.cs b/test/RestSharp.Tests.Integrated/Server/TestServer.cs
index fa7a365da..7570bdd66 100644
--- a/test/RestSharp.Tests.Integrated/Server/TestServer.cs
+++ b/test/RestSharp.Tests.Integrated/Server/TestServer.cs
@@ -37,6 +37,10 @@ public HttpServer(ITestOutputHelper output = null) {
_app.MapGet("request-echo", async context => await context.Request.BodyReader.AsStream().CopyToAsync(context.Response.BodyWriter.AsStream()));
_app.MapDelete("delete", () => new TestResponse { Message = "Works!" });
+ // Cookies
+ _app.MapGet("get-cookies", HandleCookies);
+ _app.MapGet("set-cookies", HandleSetCookies);
+
// PUT
_app.MapPut(
ContentResource,
@@ -60,6 +64,34 @@ IResult HandleHeaders(HttpContext ctx) {
return Results.Ok(response);
}
+ IResult HandleCookies(HttpContext ctx) {
+ var results = new List();
+ foreach (var (key, value) in ctx.Request.Cookies) {
+ results.Add($"{key}={value}");
+ }
+ return Results.Ok(results);
+ }
+
+ IResult HandleSetCookies(HttpContext ctx) {
+ ctx.Response.Cookies.Append("cookie1", "value1");
+ ctx.Response.Cookies.Append("cookie2", "value2", new CookieOptions {
+ Path = "/path_extra"
+ });
+ ctx.Response.Cookies.Append("cookie3", "value3", new CookieOptions {
+ Expires = DateTimeOffset.Now.AddDays(2)
+ });
+ ctx.Response.Cookies.Append("cookie4", "value4", new CookieOptions {
+ MaxAge = TimeSpan.FromSeconds(100)
+ });
+ ctx.Response.Cookies.Append("cookie5", "value5", new CookieOptions {
+ Secure = true
+ });
+ ctx.Response.Cookies.Append("cookie6", "value6", new CookieOptions {
+ HttpOnly = true
+ });
+ return Results.Content("success");
+ }
+
async Task HandleUpload(HttpRequest req) {
if (!req.HasFormContentType) {
return Results.BadRequest("It's not a form");