Workaround for SET-COOKIE bug in www.responseHeaders?

There is a bug in the way Unity handles the HTTP response headers. A typical HTTP response might look like this:

HTTP/1.1 200 OK
Content-Type: application/json
Date: Sun, 19 Oct 2014 19:48:07 GMT
Set-Cookie: session-data=a3izTzllfjmJvIAedFWH8_RF1VUoTVbszM-4KtITK8QBZwE
Set-Cookie: AWSELB=8FCF616716267D5222A365F68CB5F524241ADC40F2A79E054E56BD41E6C10E5921CA9D8BE1291E904BD712CDA6EB211CD74D6780AE1E44EE07A58093B97B9C3AA5121EF17E09420E;PATH=/
transfer-encoding: chunked
Connection: keep-alive

However, since Unity parses this as a Dictionary, only one Set-Cookie header gets parsed. In other words, only one cookie will get through and the others will be ignored / removed.

I submitted a bug report about this. In the meantime, does anyone know of a workaround?

Answering my own question. Discovered a gross hack that lets me get at the data by using reflection to access the responseHeadersString property of the WWW class, which is a protected member. Below is an extension method class I wrote that provides methods for getting, parsing and sending cookies. Enjoy!

//
// UnityCookies.cs
// by Sam McGrath
//
// Use as you please.
//
// Usage:
//    Dictionary<string,string> cookies = www.ParseCookies();
//
// To send cookies in a WWW response:
//    var www = new WWW( url, null, UnityCookies.GetCookieRequestHeader(cookies) );
//    (if other headers are needed, merge them with the dictionary returned by GetCookieRequestHeader)
//

using UnityEngine;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;

public static class UnityCookies {

	public static string GetRawCookieString( this WWW www ) {
		if ( !www.responseHeaders.ContainsKey("SET-COOKIE") ) {
			return null;
		}

		// HACK: workaround for Unity bug that doesn't allow multiple SET-COOKIE headers
		var rhsPropInfo = typeof(WWW).GetProperty( "responseHeadersString",BindingFlags.Public|BindingFlags.NonPublic|BindingFlags.Instance );
		if ( rhsPropInfo == null ) {
			Debug.LogError( "www.responseHeadersString not found in WWW class." );
			return null;
		}
		var headersString = rhsPropInfo.GetValue( www, null ) as string;
		if ( headersString == null ) {
			return null;
		}

		// concat cookie headers
		var allCookies = new StringBuilder();
		string[] lines = headersString.Split( new string[] { "

", "
" }, StringSplitOptions.RemoveEmptyEntries );
foreach( var l in lines ) {
var colIdx = l.IndexOf( ‘:’ );
if ( colIdx < 1 ) {
continue;
}
var headerType = l.Substring( 0,colIdx ).Trim();
if ( headerType.ToUpperInvariant() != “SET-COOKIE” ) {
continue;
}
var headerVal = l.Substring( colIdx+1 ).Trim();
if ( allCookies.Length > 0 ) {
allCookies.Append( "; " );
}
allCookies.Append( headerVal );
}

		return allCookies.ToString();
	}

	public static Dictionary<string,string> ParseCookies( this WWW www ) {
		return ParseCookies( www.GetRawCookieString() );
	}

	public static Dictionary<string,string> ParseCookies( string str ) {
		// cookie parsing adapted from node.js cookie module, so it should be pretty robust.
		var dict = new Dictionary<string,string>();
		if ( str != null ) {
			var pairs = Regex.Split( str, "; *" );
			foreach( var pair in pairs ) {
				var eqIdx = pair.IndexOf( '=' );
				if ( eqIdx == -1 ) {
					continue;
				}
				var key = pair.Substring( 0,eqIdx ).Trim();
				if ( dict.ContainsKey(key) ) {
					continue;
				}
				var val = pair.Substring( eqIdx+1 ).Trim();
				if ( val[0] == '"' ) {
					val = val.Substring( 1, val.Length-2 );
				}
				dict[ key ] = WWW.UnEscapeURL( val );
			}
		}

		return dict;
	}

	public static Dictionary<string,string> GetCookieRequestHeader( Dictionary<string,string> cookies ) {
		var str = new StringBuilder();
		foreach( var c in cookies ) {
			if ( str.Length > 0 )
				str.Append( "; " );
			str.Append( c.Key ).Append( '=' ).Append( WWW.EscapeURL(c.Value) );
		}
		return new Dictionary<string,string>{ {"Cookie", str.ToString() } };
	}
}

Sure, there are a few, however not all are available in all situations. If you’re in a webplayer you can use the browsers webinterface (via site-javascript code) to perform webrequests and read manually read the headers.

For the other platforms you can use any .NET / Mono web class (System.Net) that actually works with headerfields. However since those classes requires Sockets to work, it’s not available for Android and iOS Free. Only for the pro version. Standalone builds should work fine.

See the license comparison page (seciont “code”: .NET Socket Support)

You probably want to use either the HttpWebRequest if you do single requests or the WebClient class which actually handles cookies itself and is designed for multiple requests on the same domain.