How to remove charset=utf-8 from Content-Type when using PostUrlEncodedAsync - httpclient

When posting data with Flurl's PostUrlEncodedAsync, the following Content-Type is automatically set:
application/x-www-form-urlencoded; charset=utf-8
How do I remove the charset=utf-8 part?
I already tried:
flurlClient.ConfigureHttpClient(c => c.DefaultRequestHeaders.TryAddWithoutValidation("Content-Type", "application/x-www-form-urlencoded"));
but it doesnt work. Suggestion comes from: https://stackoverflow.com/a/44548514/915414

Instead of using Flurl, I tried to achieve the same goal with HttpClient. That didnt work, so I created an extension method for Flurl instead.
https://stackoverflow.com/a/44543016/915414 suggests using StringContent to change the Content-Type:
var jobInJson = JsonConvert.SerializeObject(job);
var json = new StringContent(jobInJson, Encoding.UTF8);
json.Headers.ContentType = MediaTypeHeaderValue.Parse("application/json; odata=verbose");
var flurClient = GetBaseUrlForOperations("Jobs");
return await flurClient.PostAsync(json).ReceiveJson<Job>();
Although this does change the Content-Type, the charset=utf-8 still remains.
I decompiled System.Net.Http.StringContent to see how it works. It defaults to a charset:
this.Headers.ContentType = new MediaTypeHeaderValue(mediaType == null ? "text/plain" : mediaType)
{
CharSet = encoding == null ? HttpContent.DefaultStringEncoding.WebName : encoding.WebName
};
And guess what... At its core, PostUrlEncodedAsync uses StringContent.
So, I created an extension method for Flurl, that uses a similar implementation of StringContent, where CharSet = "";
PostUrlEncodedAsyncWithoutCharset:
public static class HttpExtensions
{
public static Task<HttpResponseMessage> PostUrlEncodedAsyncWithoutCharset(this IFlurlClient client, object data, CancellationToken cancellationToken = default(CancellationToken), HttpCompletionOption completionOption = HttpCompletionOption.ResponseContentRead)
{
CapturedUrlContentCustom urlEncodedContent = new CapturedUrlContentCustom(client.Settings.UrlEncodedSerializer.Serialize(data));
return client.SendAsync(HttpMethod.Post, (HttpContent)urlEncodedContent, new CancellationToken?(cancellationToken), completionOption);
}
}
CapturedUrlContentCustom:
public class CapturedUrlContentCustom : CapturedStringContentCustom
{
public CapturedUrlContentCustom(string data)
: base(data, (Encoding) null, "application/x-www-form-urlencoded")
{
}
}
CapturedStringContentCustom:
public class CapturedStringContentCustom : CustomStringContent
{
public string Content { get; }
public CapturedStringContentCustom(string content, Encoding encoding = null, string mediaType = null)
: base(content, encoding, mediaType)
{
this.Content = content;
}
}
CustomStringContent:
public class CustomStringContent : ByteArrayContent
{
private const string defaultMediaType = "application/x-www-form-urlencoded";
public CustomStringContent(string content)
: this(content, (Encoding)null, (string)null)
{
}
public CustomStringContent(string content, Encoding encoding)
: this(content, encoding, (string)null)
{
}
public CustomStringContent(string content, Encoding encoding, string mediaType)
: base(CustomStringContent.GetContentByteArray(content, encoding))
{
this.Headers.ContentType = new MediaTypeHeaderValue(mediaType == null ? "application/x-www-form-urlencoded" : mediaType)
{
CharSet = ""
};
}
private static byte[] GetContentByteArray(string content, Encoding encoding)
{
if (content == null)
throw new ArgumentNullException(nameof(content));
if (encoding == null)
encoding = Encoding.UTF8;
return encoding.GetBytes(content);
}
}
Now, you can call PostUrlEncodedAsyncWithoutCharset and charset=utf-8 will not appear.

Related

feign client put specific content-type=image/jpeg or content-type=image/png orcontent-type=image/gif

the service provider consumes exact content-type image/jpeg or image/png ,image/gif.
if the content-type is multipart/form-data then the service provider could not prosess;
And this is my Feign Client:
#FeignClient(name = "fileUpload", configuration = MultipartSupportConfig.class, url = "https://storage.googleapis.com/myconpany")
public interface FileUploadSao {
#PutMapping(value = "", produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.IMAGE_JPEG_VALUE)
ResponseEntity<String> uploadFile(URI uri, #RequestPart(value = "file") ByteArrayOutputStream image);
}
this is the result :
feign.codec.EncodeException: Could not write request: no suitable HttpMessageConverter found for request type [java.util.LinkedHashMap] and content type [image/jpeg]
at org.springframework.cloud.openfeign.support.SpringEncoder.encodeWithMessageConverter(SpringEncoder.java:188)
at org.springframework.cloud.openfeign.support.SpringEncoder.encode(SpringEncoder.java:134)
at org.springframework.cloud.openfeign.support.PageableSpringEncoder.encode(PageableSpringEncoder.java:101)
at feign.ReflectiveFeign$BuildFormEncodedTemplateFromArgs.resolve(ReflectiveFeign.java:358)
at feign.ReflectiveFeign$BuildTemplateByResolvingArgs.create(ReflectiveFeign.java:232)
at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:84)
at feign.ReflectiveFeign$FeignInvocationHandler.invoke(ReflectiveFeign.java:100)
at jdk.proxy2/jdk.proxy2.$Proxy167.uploadFile(Unknown Source)
i have tryed the extend AbstractMessageConverter
and extend
WebMvcConfigurationSupport
#Configuration
public class WebConfig extends WebMvcConfigurationSupport {
#Override
protected void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(byteArrayHttpMessageConverter());
super.configureMessageConverters(converters);
}
#Bean
public ByteArrayHttpMessageConverter byteArrayHttpMessageConverter() {
ByteArrayHttpMessageConverter arrayHttpMessageConverter = new ByteArrayHttpMessageConverter();
arrayHttpMessageConverter.setSupportedMediaTypes(getSupportedMediaTypes());
return arrayHttpMessageConverter;
}
private List<MediaType> getSupportedMediaTypes() {
List<MediaType> list = new ArrayList<>();
list.add(MediaType.IMAGE_JPEG);
list.add(MediaType.IMAGE_PNG);
list.add(MediaType.APPLICATION_OCTET_STREAM);
return list;
}
}
i wish to send a put request with fiegnClient containing content-type=image/npg

Redirect asp.net core 2.0 urls to lowercase

I have seen that you can configure routing in ASP.NET Core 2.0 to generate lower case urls as described here: https://stackoverflow.com/a/45777372/83825
Using this:
services.AddRouting(options => options.LowercaseUrls = true);
However, although this is fine to GENERATE the urls, it doesn't appear to do anything to actually ENFORCE them, that is, redirect any urls that are NOT all lowercase to the corresponding lowercase url (preferably via 301 redirect).
I know people are accessing my site via differently cased urls, and I want them to all be lowercase, permanently.
Is doing a standard redirect via RewriteOptions and Regex the only way to do this? what would be the appropriate expression to do this:
var options = new RewriteOptions().AddRedirect("???", "????");
Or is there another way?
I appreciate this is many months old, however for people who may be looking for the same solution, you can add a complex redirect implementing IRule such as:
public class RedirectLowerCaseRule : IRule
{
public int StatusCode { get; } = (int)HttpStatusCode.MovedPermanently;
public void ApplyRule(RewriteContext context)
{
HttpRequest request = context.HttpContext.Request;
PathString path = context.HttpContext.Request.Path;
HostString host = context.HttpContext.Request.Host;
if (path.HasValue && path.Value.Any(char.IsUpper) || host.HasValue && host.Value.Any(char.IsUpper))
{
HttpResponse response = context.HttpContext.Response;
response.StatusCode = StatusCode;
response.Headers[HeaderNames.Location] = (request.Scheme + "://" + host.Value + request.PathBase.Value + request.Path.Value).ToLower() + request.QueryString;
context.Result = RuleResult.EndResponse;
}
else
{
context.Result = RuleResult.ContinueRules;
}
}
}
This can then be applied in your Startup.cs under Configure method as such:
new RewriteOptions().Add(new RedirectLowerCaseRule());
Slightly different implementation, also inspired from this other thread.
public class RedirectLowerCaseRule : IRule
{
public void ApplyRule(RewriteContext context)
{
HttpRequest request = context.HttpContext.Request;
string url = request.Scheme + "://" + request.Host + request.PathBase + request.Path;
bool isGet = request.Method.ToLowerInvariant().Contains("get");
if ( isGet && url.Contains(".") == false && Regex.IsMatch(url, #"[A-Z]") )
{
HttpResponse response = context.HttpContext.Response;
response.Clear();
response.StatusCode = StatusCodes.Status301MovedPermanently;
response.Headers[HeaderNames.Location] = url.ToLowerInvariant() + request.QueryString;
context.Result = RuleResult.EndResponse;
}
else
{
context.Result = RuleResult.ContinueRules;
}
}
}
Changes made that I find useful:
Using ToLowerInvariant() instead of ToLower() (see possible issues here)
Keeping the port number in place.
Bypassing request methods other than GET.
Bypassing requests with a dot, assuming static files like js/css/images etc should keep any uppercase in place.
Using Microsoft.AspNetCore.Http.StatusCodes.
Adding as answer because I can't comment (yet). This is an addition to Ben Maxfields answer.
Using his code http://www.example.org/Example/example would NOT be redirected, since PathBase was not checked for uppercase letters (even though it was used to build the new lowercase URI).
So based on his code, I ended up using this:
public class RedirectLowerCaseRule : IRule
{
public int StatusCode { get; } = (int)HttpStatusCode.MovedPermanently;
public void ApplyRule(RewriteContext context)
{
HttpRequest request = context.HttpContext.Request;
PathString path = context.HttpContext.Request.Path;
PathString pathbase = context.HttpContext.Request.PathBase;
HostString host = context.HttpContext.Request.Host;
if ((path.HasValue && path.Value.Any(char.IsUpper)) || (host.HasValue && host.Value.Any(char.IsUpper)) || (pathbase.HasValue && pathbase.Value.Any(char.IsUpper)))
{
Console.WriteLine("Redirect should happen");
HttpResponse response = context.HttpContext.Response;
response.StatusCode = StatusCode;
response.Headers[HeaderNames.Location] = (request.Scheme + "://" + host.Value + request.PathBase + request.Path).ToLower() + request.QueryString;
context.Result = RuleResult.EndResponse;
}
else
{
context.Result = RuleResult.ContinueRules;
}
}
}
My two cents... based on https://github.com/aspnet/AspNetCore/blob/master/src/Middleware/Rewrite/src/RedirectToWwwRule.cs
public class RedirectToLowercaseRule : IRule
{
private readonly int _statusCode;
public RedirectToLowercaseRule(int statusCode)
{
_statusCode = statusCode;
}
public void ApplyRule(RewriteContext context)
{
var req = context.HttpContext.Request;
if (!req.Scheme.Any(char.IsUpper)
&& !req.Host.Value.Any(char.IsUpper)
&& !req.PathBase.Value.Any(char.IsUpper)
&& !req.Path.Value.Any(char.IsUpper))
{
context.Result = RuleResult.ContinueRules;
return;
}
var newUrl = UriHelper.BuildAbsolute(req.Scheme.ToLowerInvariant(), new HostString(req.Host.Value.ToLowerInvariant()), req.PathBase.Value.ToLowerInvariant(), req.Path.Value.ToLowerInvariant(), req.QueryString);
var response = context.HttpContext.Response;
response.StatusCode = _statusCode;
response.Headers[HeaderNames.Location] = newUrl;
context.Result = RuleResult.EndResponse;
context.Logger.RedirectedToLowercase();
}
}
With extension methods:
public static class RewriteOptionsExtensions
{
public static RewriteOptions AddRedirectToLowercase(this RewriteOptions options, int statusCode)
{
options.Add(new RedirectToLowercaseRule(statusCode));
return options;
}
public static RewriteOptions AddRedirectToLowercase(this RewriteOptions options)
{
return AddRedirectToLowercase(options, StatusCodes.Status307TemporaryRedirect);
}
public static RewriteOptions AddRedirectToLowercasePermanent(this RewriteOptions options)
{
return AddRedirectToLowercase(options, StatusCodes.Status308PermanentRedirect);
}
}
And logging:
internal static class MiddlewareLoggingExtensions
{
private static readonly Action<ILogger, Exception> _redirectedToLowercase = LoggerMessage.Define(LogLevel.Information, new EventId(1, "RedirectedToLowercase"), "Request redirected to lowercase");
public static void RedirectedToLowercase(this ILogger logger)
{
_redirectedToLowercase(logger, null);
}
}
And usage:
app.UseRewriter(new RewriteOptions()
.AddRedirectToLowercase());
Other considerations are the choice of status codes. I've used 307 and 308 in the extension methods as these prevent the request method being changed (e.g. from GET to POST) during the request, however if you want to allow that behaviour you can use 301 and 302. See What's the difference between HTTP 301 and 308 status codes? for further information.
Are you sure you want a redirect? If not, and your goal is that there is no such thing as uppercase in your host and path, you can use the following IRule. This assures me that wherever I look at the path in the pipeline that it is lowercase.
public class RewriteLowerCaseRule : IRule
{
public void ApplyRule(RewriteContext context)
{
var request = context.HttpContext.Request;
var host = request.Host;
var pathBase = request.PathBase;
var path = request.Path;
if (host.HasValue)
{
if (host.Port == null)
{
request.Host = new HostString(host.Host.ToLower());
}
else
{
request.Host = new HostString(host.Host.ToLower(), (int) host.Port);
}
}
if (pathBase.HasValue)
{
request.PathBase = new PathString(pathBase.Value.ToLower());
}
if (path.HasValue)
{
request.Path = new PathString(path.Value.ToLower());
request.PathBase = new PathString(pathBase.Value.ToLower());
}
context.Result = RuleResult.ContinueRules;
}
}
Usage:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
app.UseRewriter(new RewriteOptions().Add(new RewriteLowerCaseRule()));
...
}

SprintBoot returning a PNG from a Controller's RequestMapping

I've been scouring the internet for resources and I feel like I almost have the answer, but I can't quite seem to get a BufferedImage to be returned to a browser window.
The project generates a maze which can then create a BufferedImage.
Here is the code from my Controller.
#RequestMapping(method = RequestMethod.GET, path = "/image", params = {"rows", "columns"})
public ResponseEntity<byte[]> image(#RequestParam(name = "rows") int rows, #RequestParam(name = "columns") int columns) throws IOException, InterruptedException {
try {
BasicCartesianGrid requestedMaze = new BasicCartesianGrid(rows, columns);
requestedMaze.forEach(CellAlgorithms.BINARY_TREE);
BufferedImage bufferedImage = requestedMaze.toDisplayImage();
{ // Dumping to file for debugging <- this works as expected
File outputFile = new File("save.png");
ImageIO.write(bufferedImage, "png", outputFile);
}
ByteArrayOutputStream pngByteStream = new ByteArrayOutputStream();
ImageIO.write(bufferedImage, "png", pngByteStream);
byte[] pngBytes = pngByteStream.toByteArray();
final HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.IMAGE_PNG);
headers.setContentLength(pngBytes.length);
headers.setCacheControl(CacheControl.noCache().getHeaderValue());
return new ResponseEntity<>(pngBytes, headers, HttpStatus.OK);
} catch (Exception e) {
// This hasn't occurred yet, but is for just in case
Thread.sleep(1000);
System.err.println(e.getLocalizedMessage());
final HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_PLAIN);
return new ResponseEntity<>(e.getLocalizedMessage().getBytes("ASCII"), headers, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
I have ascertained that the PNG is being generated correctly, as the file exists and is viewable on my hard-drive. My browser gets a broken image returned back. From my terminal, I can get some more information.
curl "http://localhost:8080/maze/image?rows=10&columns=10"
Dumps out the following (the quotation marks are part of the response, while the data represented by the ellipsis changes from request to request, due to the fact each maze is randomly generated and unique):
"iVBORw0KGgoAAAANSUhEUgAAA......"
I googled this string prefix, and found this page. Which shows that this string should be used as a data-uri, like so:
<img src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA…" >
I'm not sure where to go from here. It seems like my image is being generated correctly, but I must be missing a header in my response to tell the browser/spring that these bytes should be interpreted as an image and not as just a string.
UPDATE:
Based on the dialog between myself and Shawn Clark from the answer section, here is what I have presently.
#SpringBootApplication
#Log4j
public class SpringMazesApplication {
#Bean
public HttpMessageConverter<BufferedImage> bufferedImageHttpMessageConverter() {
log.debug("Registering BufferedImage converter");
return new BufferedImageHttpMessageConverter();
}
public static void main(String[] args) throws IOException {
SpringApplication.run(SpringMazesApplication.class, args);
}
}
And the actual controller:
#Controller
#RequestMapping(path = "/maze/basic", method = RequestMethod.GET)
#Log4j
public class BasicMazeController {
#RequestMapping(params = {"format", "format=text"}, produces = MediaType.TEXT_PLAIN_VALUE)
#ResponseBody
public String simpleMazeText(#RequestParam(name = "rows", defaultValue = "10", required = false) int rows,
#RequestParam(name = "columns", defaultValue = "10", required = false) int columns) throws IOException {
BasicCartesianGrid requestedMaze = new BasicCartesianGrid(rows, columns);
requestedMaze.forEach(CellAlgorithms.BINARY_TREE);
return requestedMaze.toDisplayString();
}
#RequestMapping(params = {"format=image"}, produces = MediaType.IMAGE_PNG_VALUE)
#ResponseBody
public BufferedImage simpleMazeImage(#RequestParam(name = "rows", defaultValue = "10", required = false) int rows,
#RequestParam(name = "columns", defaultValue = "10", required = false) int columns) throws IOException {
log.debug("Starting image generation");
BasicCartesianGrid requestedMaze = new BasicCartesianGrid(rows, columns);
requestedMaze.forEach(CellAlgorithms.BINARY_TREE);
BufferedImage bufferedImage = requestedMaze.toDisplayImage();
{ // Dumping to file for debugging <- this works as expected
log.debug("Dumping image to hd");
File outputFile = new File("save.png");
ImageIO.write(bufferedImage, "png", outputFile);
}
log.debug("Returning from image generation");
return bufferedImage;
}
#RequestMapping
#ResponseBody
public ResponseEntity<String> simpleMazeInvalid(#RequestParam(name = "rows", defaultValue = "10", required = false) int rows,
#RequestParam(name = "columns", defaultValue = "10", required = false) int columns,
#RequestParam(name = "format") String format) throws IOException {
final HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.TEXT_PLAIN);
return new ResponseEntity<>("Invalid format: " + format, headers, HttpStatus.BAD_REQUEST);
}
}
From my terminal I can curl -D - "url" and I can see with both logging/debugging and the output from my terminal, that the converter is properly registered at the begging of the application and that I'm getting responses as you would expect from all but the actual image uri which returns a 406 Not Acceptable. If I remove the #ResponseBody from the image method, it just returns a 500. I can verify that the image is properly generated as it is being written to disk as I expect it should.
Check out the produces attribute on the #RequestMapping. You would want to set it to image/png.
Here is a complete example:
#RestController
public class ProduceImage {
#GetMapping(path = "/image", produces = "image/png")
public BufferedImage image() throws Exception {
BufferedImage bufferedImage = ImageIO.read(new File("E:\\Downloads\\skin_201305121633211421.png"));
return bufferedImage;
}
}
My BufferedImage is something from my computer but it can be just as easily the BufferedImage that you have from the requestedMaze.toDisplayImage() without having to do all that other work. To make this work you want to include the BufferedImageHttpMessageConverter in your context.
#Bean
public HttpMessageConverter<BufferedImage> bufferedImageHttpMessageConverter() {
return new BufferedImageHttpMessageConverter();
}

Unity3D WWW Post data async

I want to post a JSON to a website using the WWW class, But I get this answer from the server: "Synchronization problem.". Is there a way to change from sync to async? Thank You
You can run your WWW job in a coroutine (WWW supports this well):
using UnityEngine;
public class PostJSON : MonoBehaviour {
void Start () {
string url = "http://your_url_endpoint";
WWWForm form = new WWWForm();
Hashtable headers = form.headers;
headers["Content-Type"] = "application/json";
Hashtable data = new Hashtable();
data["message"] = "a sample message sent to service as json";
string json = JSON.JsonEncode(data);
byte[] bytes = Encoding.UTF8.GetBytes(json);
WWW www = new WWW(url, bytes, headers);
StartCoroutine(WaitForRequest(www));
}
IEnumerator WaitForRequest(WWW www)
{
yield return www
// check for errors
if (www.error == null)
{
Debug.Log("WWW Ok!: " + www.data);
} else {
Debug.Log("WWW Error: "+ www.error);
}
}
}
Here you have a running project which I use to talk to a json based REST service called KiiCloud:
http://blog.kii.com/?p=2939
HTH
The answer from German was very helpful, but I made some tweaks so that it'll compile and run (with sample serialization / deserialization bits).
Just pass in the BaseUrl you want to post to, i.e.
http://www.domain.com/somecontroller/someaction or whatever.
using System;
using System.Text;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
[Serializable]
public class Person
{
public string Name;
}
[Serializable]
public class Response
{
public string SomeValue;
}
public class PostJSON : MonoBehaviour
{
public string BaseUrl;
private WWWForm form;
private Dictionary<string, string> headers = null;
void Start ()
{
var basUrlNotSpecified = string.IsNullOrEmpty(BaseUrl);
if(basUrlNotSpecified)
{
Debug.LogWarning("BaseUrl value not specified. Post abandoned.");
return;
}
form = new WWWForm();
headers = form.headers;
headers["Content-Type"] = "application/json";
headers["Accept"] = "application/json";
var person = new Person
{
Name = "Iulian Palade"
};
var json = JsonUtility.ToJson(person);
byte[] bytes = Encoding.UTF8.GetBytes(json);
WWW www = new WWW(BaseUrl, bytes, headers);
StartCoroutine(WaitForRequest(www));
}
IEnumerator WaitForRequest(WWW www)
{
yield return www;
if (www.error == null)
{
Debug.Log("WWW Ok!: " + www.text);
var response = JsonUtility.FromJson<Response>(www.text);
Debug.Log(response.SomeValue);
}
else
{
Debug.Log("WWW Error: "+ www.error);
}
}
}

ASP.NET MVC TempData in browser cookie

I am trying to use a custom ITempDataProvider provider to store TempData in a browser's cookie instead of session state. However, everything works fine except that I am unable to remove the cookie from the Response stream after reading it.
Any ideas?
Thanks!
public class CookieTempDataProvider : ITempDataProvider
{
internal const string TempDataCookieKey = "__ControllerTempData";
HttpContextBase _httpContext;
public CookieTempDataProvider(HttpContextBase httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException("httpContext");
}
_httpContext = httpContext;
}
public HttpContextBase HttpContext
{
get
{
return _httpContext;
}
}
protected virtual IDictionary<string, object> LoadTempData(ControllerContext controllerContext)
{
HttpCookie cookie = _httpContext.Request.Cookies[TempDataCookieKey];
if (cookie != null && !string.IsNullOrEmpty(cookie.Value))
{
IDictionary<string, object> deserializedTempData = DeserializeTempData(cookie.Value);
// Remove cookie
cookie.Expires = DateTime.MinValue;
cookie.Value = string.Empty;
_httpContext.Request.Cookies.Remove(TempDataCookieKey);
if (_httpContext.Response != null && _httpContext.Response.Cookies != null)
{
HttpCookie responseCookie = _httpContext.Response.Cookies[TempDataCookieKey];
if (responseCookie != null)
{
// Remove cookie
cookie.Expires = DateTime.MinValue;
cookie.Value = string.Empty;
_httpContext.Response.Cookies.Remove(TempDataCookieKey);
}
}
return deserializedTempData;
}
return new Dictionary<string, object>();
}
protected virtual void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values)
{
string cookieValue = SerializeToBase64EncodedString(values);
var cookie = new HttpCookie(TempDataCookieKey);
cookie.HttpOnly = true;
cookie.Value = cookieValue;
_httpContext.Response.Cookies.Add(cookie);
}
public static IDictionary<string, object> DeserializeTempData(string base64EncodedSerializedTempData)
{
byte[] bytes = Convert.FromBase64String(base64EncodedSerializedTempData);
var memStream = new MemoryStream(bytes);
var binFormatter = new BinaryFormatter();
return binFormatter.Deserialize(memStream, null) as IDictionary<string, object> /*TempDataDictionary : This returns NULL*/;
}
public static string SerializeToBase64EncodedString(IDictionary<string, object> values)
{
MemoryStream memStream = new MemoryStream();
memStream.Seek(0, SeekOrigin.Begin);
var binFormatter = new BinaryFormatter();
binFormatter.Serialize(memStream, values);
memStream.Seek(0, SeekOrigin.Begin);
byte[] bytes = memStream.ToArray();
return Convert.ToBase64String(bytes);
}
IDictionary<string, object> ITempDataProvider.LoadTempData(ControllerContext controllerContext)
{
return LoadTempData(controllerContext);
}
void ITempDataProvider.SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values)
{
SaveTempData(controllerContext, values);
}
}
There is a better solution by Brock Allen on GitHub that uses encryption, 2 forms of serialization, and compression to protect and optimize the cookies.
https://github.com/brockallen/CookieTempData
Here is a link to the blog about it:
http://brockallen.com/2012/06/11/cookie-based-tempdata-provider/
He also has a good technique using IControllerFactory to ensure every controller is supplied with an instance of ITempDataProvider.
Hi I too had the same issue and it was an issue with the implementation of CookieTempDataProvider.
So I modified the code a bit and now it works perfectly.
When it reads the data from the cookie, it removes it from both the request and response. But add another cookie with an empty value in the SaveData function which is called when the request processing is completed.
Points to note : If you want to remove a cookie, you have to set the timeout value and send it back to the client and then the browser will remove it. We cannot do it otherwise from the code a the cookie is handled by the browser
And I found out that setting the expiration to DateTime.MinValue does not expire the cookie in chrome (don't know about the other browsers) so I set it to 2001-01-01 :)
Here is the working code
public class CookieTempDataProvider : ITempDataProvider
{
internal const string TempDataCookieKey = "__ControllerTempData";
HttpContextBase _httpContext;
public CookieTempDataProvider(HttpContextBase httpContext)
{
if (httpContext == null)
{
throw new ArgumentNullException("httpContext");
}
_httpContext = httpContext;
}
public HttpContextBase HttpContext
{
get
{
return _httpContext;
}
}
protected virtual IDictionary<string, object> LoadTempData(ControllerContext controllerContext)
{
if (_httpContext.Request.Cookies.AllKeys.Contains(TempDataCookieKey)) //we need this because
//Cookies[TempDataCookieKey] will create the cookie if it does not exist
{
HttpCookie cookie = _httpContext.Request.Cookies[TempDataCookieKey];
if (cookie != null && !string.IsNullOrEmpty(cookie.Value))
{
IDictionary<string, object> deserializedTempData = DeserializeTempData(cookie.Value);
// Remove cookie
cookie.Expires = new DateTime(2000, 1, 1);
cookie.Value = string.Empty;
_httpContext.Request.Cookies.Remove(TempDataCookieKey);
if (_httpContext.Response != null && _httpContext.Response.Cookies != null)
{
HttpCookie responseCookie = _httpContext.Response.Cookies[TempDataCookieKey];
if (responseCookie != null)
{
// Remove cookie
cookie.Expires = new DateTime(2000, 1, 1);
cookie.Value = string.Empty;
_httpContext.Response.Cookies.Remove(TempDataCookieKey);
}
}
return deserializedTempData;
}
}
return new Dictionary<string, object>();
}
protected virtual void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values)
{
if (values != null && values.Count > 0)
{
//there are values to set, so add the cookie. But no need to expire it as we need the browser to send the
//cookie back with the next request
string cookieValue = SerializeToBase64EncodedString(values);
var cookie = new HttpCookie(TempDataCookieKey);
cookie.HttpOnly = true;
cookie.Value = cookieValue;
_httpContext.Response.Cookies.Add(cookie);
}
else
{
//Still we need to add the cookie with the expiration set, to make the client browser remove the cookie from the request.
//Otherwise the browser will continue to send the cookie with the response
//Also we need to do this only if the requet had a tempdata cookie
if (_httpContext.Request.Cookies.AllKeys.Contains(TempDataCookieKey))
{
{
HttpCookie cookie = _httpContext.Request.Cookies[TempDataCookieKey];
// Remove the request cookie
cookie.Expires = new DateTime(2000, 1, 1);
cookie.Value = string.Empty;
_httpContext.Request.Cookies.Remove(TempDataCookieKey);
var rescookie = new HttpCookie(TempDataCookieKey);
rescookie.HttpOnly = true;
rescookie.Value = "";
rescookie.Expires = new DateTime(2000, 1, 1); //so that the browser will remove the cookie when it receives the request
_httpContext.Response.Cookies.Add(rescookie);
}
}
}
}
public static IDictionary<string, object> DeserializeTempData(string base64EncodedSerializedTempData)
{
byte[] bytes = Convert.FromBase64String(base64EncodedSerializedTempData);
var memStream = new MemoryStream(bytes);
var binFormatter = new BinaryFormatter();
return binFormatter.Deserialize(memStream, null) as IDictionary<string, object> /*TempDataDictionary : This returns NULL*/;
}
public static string SerializeToBase64EncodedString(IDictionary<string, object> values)
{
MemoryStream memStream = new MemoryStream();
memStream.Seek(0, SeekOrigin.Begin);
var binFormatter = new BinaryFormatter();
binFormatter.Serialize(memStream, values);
memStream.Seek(0, SeekOrigin.Begin);
byte[] bytes = memStream.ToArray();
return Convert.ToBase64String(bytes);
}
IDictionary<string, object> ITempDataProvider.LoadTempData(ControllerContext controllerContext)
{
return LoadTempData(controllerContext);
}
void ITempDataProvider.SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values)
{
SaveTempData(controllerContext, values);
}
}
Here is an example of a working solution without lots of excess code. It uses Json.NET for serializing, which is faster than BinaryFormatter + Base64Encoding and also produces a much shorter string (=less http overhead).
public class CookieTempDataProvider : ITempDataProvider
{
const string cookieKey = "temp";
public IDictionary<string, object> LoadTempData(ControllerContext controllerContext)
{
var cookie = controllerContext.HttpContext.Request.Cookies[cookieKey];
if (cookie != null) {
return JsonConvert.DeserializeObject<IDictionary<string, object>>(cookie.Value);
}
return null;
}
// Method is called after action execution. The dictionary mirrors the contents of TempData.
// If there are any values in the dictionary, save it in a cookie. If the dictionary is empty,
// remove the cookie if it exists.
public void SaveTempData(ControllerContext controllerContext, IDictionary<string, object> values)
{
var ctx = controllerContext.HttpContext;
if (values.Count > 0) {
var cookie = new HttpCookie(cookieKey)
{
HttpOnly = true,
Value = JsonConvert.SerializeObject(values)
};
ctx.Response.Cookies.Add(cookie);
} else if (ctx.Request.Cookies[cookieKey] != null) {
// Expire cookie to remove it from browser.
ctx.Response.Cookies[cookieKey].Expires = DateTime.Today.AddDays(-1);
}
}
}