Skip to content

Commit 5d46685

Browse files
authored
[Mono.Android] Tweak AndroidMessageHandler behavior for WCF support (#7785)
Context: #7230 Context: dotnet/runtime#80935 When a WCF application invokes an endpoint which returns compressed content, and `AndroidMessageHandler` is doing the network requests ([the default when `$(UseNativeHttpHandler)`=True][0]): var soapClient = new WebServiceSoapClient(WebServiceSoapClient.EndpointConfiguration.WebServiceSoap); //Async test var helloResponse = await soapClient.HelloWorldAsync(); then the method will throw: The formatter threw an exception while trying to deserialize the message: There was an error while trying to deserialize parameter http://tempuri.org/:HelloWorldResponse. ---> There was an error deserializing the object of type ServiceReference1.HelloWorldResponseBody. Unexpected end of file. Following elements are not closed: HelloWorldResult, HelloWorldResponse, Body, Envelope. Line 1, position 298. at System.Runtime.Serialization.XmlObjectSerializer.ReadObjectHandleExceptions(XmlReaderDelegator reader, Boolean verifyObjectName, DataContractResolver dataContractResolver) at System.Runtime.Serialization.XmlObjectSerializer.ReadObjectHandleExceptions(XmlReaderDelegator reader, Boolean verifyObjectName) at System.Runtime.Serialization.DataContractSerializer.ReadObject(XmlDictionaryReader reader, Boolean verifyObjectName) at System.ServiceModel.Dispatcher.DataContractSerializerOperationFormatter.PartInfo.ReadObject(XmlDictionaryReader reader, XmlObjectSerializer serializer) in /_/src/System.Private.ServiceModel/src/System/ServiceModel/Dispatcher/DataContractSerializerOperationFormatter.cs:line 657 at System.ServiceModel.Dispatcher.DataContractSerializerOperationFormatter.PartInfo.ReadObject(XmlDictionaryReader reader) in /_/src/System.Private.ServiceModel/src/System/ServiceModel/Dispatcher/DataContractSerializerOperationFormatter.cs:line 652 at System.ServiceModel.Dispatcher.DataContractSerializerOperationFormatter.DeserializeParameterPart(XmlDictionaryReader reader, PartInfo part, Boolean isRequest) in /_/src/System.Private.ServiceModel/src/System/ServiceModel/Dispatcher/DataContractSerializerOperationFormatter.cs:line 521 The reason for this is that when `AndroidMessageHandler` creates a wrapping decompression stream, it does not update `Content-Length` to match the length of the decoded content, because it doesn't have a way to know what the length is without first reading the stream to the end, and that might prevent the end user to read the content. (Additionally, I think the `Content-Length` header should reflect the *original* content length, for the end user to be able to interpret the response as it was sent.) WCF, on the other hand, looks at the `Content-Length` header and, if found, takes the value and reads only that many bytes from the content stream and no more, which will almost always result in short reads and failure to correctly interpret the response. Workaround this issue by making `AndroidMessageHandler` behave the same way as other handlers implemented in the BCL. What they do in this situation is remove the `Content-Length` header, making WCF read the stream to the end. Additionally, the clients remove the compressed content encoding identifier from the `Content-Encoding` header. var handler = new AndroidMessageHandler { AutomaticDecompression = DecompressionMethods.All }; var client = new HttpClient (handler); var response = await client.GetAsync ("https://httpbin.org/gzip"); // response.Content.Headers won't contain Content-Length, // and response.Content.Headers.ContentEncoding won't contain `gzip`. As a bonus, also adds support for decompression of responses compressed with the `Brotli` compression which use the `br` encoding ID in the `Content-Encoding` header. [0]: https://learn.microsoft.com/en-us/dotnet/core/deploying/trimming/trimming-options?pivots=dotnet-7-0
1 parent f007593 commit 5d46685

3 files changed

Lines changed: 203 additions & 42 deletions

File tree

src/Mono.Android/Xamarin.Android.Net/AndroidMessageHandler.cs

Lines changed: 122 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,46 @@ sealed class RequestRedirectionState
6969
public bool MethodChanged;
7070
}
7171

72+
/// <summary>
73+
/// Some requests require modification to the set of headers returned from the native client.
74+
/// However, the headers collection in it is immutable, so we need to perform the adjustments
75+
/// in CopyHeaders. This class describes the necessary operations.
76+
/// </summary>
77+
sealed class ContentState
78+
{
79+
public bool? RemoveContentLengthHeader;
80+
81+
/// <summary>
82+
/// If this is `true`, then `NewContentEncodingHeaderValue` is entirely ignored
83+
/// </summary>
84+
public bool? RemoveContentEncodingHeader;
85+
86+
/// <summary>
87+
/// New 'Content-Encoding' header value. Ignored if not null and empty.
88+
/// </summary>
89+
public List<string>? NewContentEncodingHeaderValue;
90+
91+
/// <summary>
92+
/// Reset the class to values that indicate there's no action to take. MUST be
93+
/// called BEFORE any of the class members are assigned values and AFTER the state
94+
/// modification is applied
95+
/// </summary>
96+
public void Reset ()
97+
{
98+
RemoveContentEncodingHeader = null;
99+
RemoveContentLengthHeader = null;
100+
NewContentEncodingHeaderValue = null;
101+
}
102+
}
103+
72104
internal const string LOG_APP = "monodroid-net";
73105

74106
const string GZIP_ENCODING = "gzip";
75107
const string DEFLATE_ENCODING = "deflate";
108+
const string BROTLI_ENCODING = "br";
76109
const string IDENTITY_ENCODING = "identity";
110+
const string ContentEncodingHeaderName = "Content-Encoding";
111+
const string ContentLengthHeaderName = "Content-Length";
77112

78113
static readonly IDictionary<string, string> headerSeparators = new Dictionary<string, string> {
79114
["User-Agent"] = " ",
@@ -82,9 +117,9 @@ sealed class RequestRedirectionState
82117
static readonly HashSet <string> known_content_headers = new HashSet <string> (StringComparer.OrdinalIgnoreCase) {
83118
"Allow",
84119
"Content-Disposition",
85-
"Content-Encoding",
120+
ContentEncodingHeaderName,
86121
"Content-Language",
87-
"Content-Length",
122+
ContentLengthHeaderName,
88123
"Content-Location",
89124
"Content-MD5",
90125
"Content-Range",
@@ -571,6 +606,7 @@ internal Task WriteRequestContentToOutputInternal (HttpRequestMessage request, H
571606
CancellationTokenRegistration cancelRegistration = default (CancellationTokenRegistration);
572607
HttpStatusCode statusCode = HttpStatusCode.OK;
573608
Uri? connectionUri = null;
609+
var contentState = new ContentState ();
574610

575611
try {
576612
cancelRegistration = cancellationToken.Register (() => {
@@ -608,13 +644,13 @@ internal Task WriteRequestContentToOutputInternal (HttpRequestMessage request, H
608644
if (!IsErrorStatusCode (statusCode)) {
609645
if (Logger.LogNet)
610646
Logger.Log (LogLevel.Info, LOG_APP, $"Reading...");
611-
ret.Content = GetContent (httpConnection, httpConnection.InputStream!);
647+
ret.Content = GetContent (httpConnection, httpConnection.InputStream!, contentState);
612648
} else {
613649
if (Logger.LogNet)
614650
Logger.Log (LogLevel.Info, LOG_APP, $"Status code is {statusCode}, reading...");
615651
// For 400 >= response code <= 599 the Java client throws the FileNotFound exception when attempting to read from the input stream.
616652
// Instead we try to read the error stream and return an empty string if the error stream isn't readable.
617-
ret.Content = GetErrorContent (httpConnection, new StringContent (String.Empty, Encoding.ASCII));
653+
ret.Content = GetErrorContent (httpConnection, new StringContent (String.Empty, Encoding.ASCII), contentState);
618654
}
619655

620656
bool disposeRet;
@@ -633,7 +669,7 @@ internal Task WriteRequestContentToOutputInternal (HttpRequestMessage request, H
633669
}
634670
}
635671

636-
CopyHeaders (httpConnection, ret);
672+
CopyHeaders (httpConnection, ret, contentState);
637673
ParseCookies (ret, connectionUri);
638674

639675
if (disposeRet) {
@@ -661,8 +697,8 @@ internal Task WriteRequestContentToOutputInternal (HttpRequestMessage request, H
661697
// We return the body of the response too, but the Java client will throw
662698
// a FileNotFound exception if we attempt to access the input stream.
663699
// Instead we try to read the error stream and return an default message if the error stream isn't readable.
664-
ret.Content = GetErrorContent (httpConnection, new StringContent ("Unauthorized", Encoding.ASCII));
665-
CopyHeaders (httpConnection, ret);
700+
ret.Content = GetErrorContent (httpConnection, new StringContent ("Unauthorized", Encoding.ASCII), contentState);
701+
CopyHeaders (httpConnection, ret, contentState);
666702

667703
if (ret.Headers.WwwAuthenticate != null) {
668704
ProxyAuthenticationRequested = false;
@@ -676,37 +712,65 @@ internal Task WriteRequestContentToOutputInternal (HttpRequestMessage request, H
676712
return ret;
677713
}
678714

679-
CopyHeaders (httpConnection, ret);
715+
CopyHeaders (httpConnection, ret, contentState);
680716
ParseCookies (ret, connectionUri);
681717

682718
if (Logger.LogNet)
683719
Logger.Log (LogLevel.Info, LOG_APP, $"Returning");
684720
return ret;
685721
}
686722

687-
HttpContent GetErrorContent (HttpURLConnection httpConnection, HttpContent fallbackContent)
723+
HttpContent GetErrorContent (HttpURLConnection httpConnection, HttpContent fallbackContent, ContentState contentState)
688724
{
689725
var contentStream = httpConnection.ErrorStream;
690726

691727
if (contentStream != null) {
692-
return GetContent (httpConnection, contentStream);
728+
return GetContent (httpConnection, contentStream, contentState);
693729
}
694730

695731
return fallbackContent;
696732
}
697733

698-
HttpContent GetContent (URLConnection httpConnection, Stream contentStream)
734+
Stream GetDecompressionWrapper (URLConnection httpConnection, Stream inputStream, ContentState contentState)
699735
{
700-
Stream inputStream = new BufferedStream (contentStream);
701-
if (decompress_here) {
702-
var encodings = httpConnection.ContentEncoding?.Split (',');
703-
if (encodings != null) {
704-
if (encodings.Contains (GZIP_ENCODING, StringComparer.OrdinalIgnoreCase))
705-
inputStream = new GZipStream (inputStream, CompressionMode.Decompress);
706-
else if (encodings.Contains (DEFLATE_ENCODING, StringComparer.OrdinalIgnoreCase))
707-
inputStream = new DeflateStream (inputStream, CompressionMode.Decompress);
736+
contentState.Reset ();
737+
if (!decompress_here || String.IsNullOrEmpty (httpConnection.ContentEncoding)) {
738+
return inputStream;
739+
}
740+
741+
var encodings = new HashSet<string> (httpConnection.ContentEncoding?.Split (','), StringComparer.OrdinalIgnoreCase);
742+
Stream? ret = null;
743+
string? supportedEncoding = null;
744+
if (encodings.Contains (GZIP_ENCODING)) {
745+
supportedEncoding = GZIP_ENCODING;
746+
ret = new GZipStream (inputStream, CompressionMode.Decompress);
747+
} else if (encodings.Contains (DEFLATE_ENCODING)) {
748+
supportedEncoding = DEFLATE_ENCODING;
749+
ret = new DeflateStream (inputStream, CompressionMode.Decompress);
750+
}
751+
#if NETCOREAPP
752+
else if (encodings.Contains (BROTLI_ENCODING)) {
753+
supportedEncoding = BROTLI_ENCODING;
754+
ret = new BrotliStream (inputStream, CompressionMode.Decompress);
755+
}
756+
#endif
757+
if (!String.IsNullOrEmpty (supportedEncoding)) {
758+
contentState.RemoveContentLengthHeader = true;
759+
760+
encodings.Remove (supportedEncoding!);
761+
if (encodings.Count == 0) {
762+
contentState.RemoveContentEncodingHeader = true;
763+
} else {
764+
contentState.NewContentEncodingHeaderValue = new List<string> (encodings);
708765
}
709766
}
767+
768+
return ret ?? inputStream;
769+
}
770+
771+
HttpContent GetContent (URLConnection httpConnection, Stream contentStream, ContentState contentState)
772+
{
773+
Stream inputStream = GetDecompressionWrapper (httpConnection, new BufferedStream (contentStream), contentState);
710774
return new StreamContent (inputStream);
711775
}
712776

@@ -881,9 +945,13 @@ void ParseCookies (AndroidHttpResponseMessage ret, Uri connectionUri)
881945
}
882946
}
883947

884-
void CopyHeaders (HttpURLConnection httpConnection, HttpResponseMessage response)
948+
void CopyHeaders (HttpURLConnection httpConnection, HttpResponseMessage response, ContentState contentState)
885949
{
886950
var headers = httpConnection.HeaderFields;
951+
bool removeContentLength = contentState.RemoveContentLengthHeader ?? false;
952+
bool removeContentEncoding = contentState.RemoveContentEncodingHeader ?? false;
953+
bool setNewContentEncodingValue = !removeContentEncoding && contentState.NewContentEncodingHeaderValue != null && contentState.NewContentEncodingHeaderValue.Count > 0;
954+
887955
foreach (var key in headers!.Keys) {
888956
if (key == null) // First header entry has null key, it corresponds to the response message
889957
continue;
@@ -895,8 +963,25 @@ void CopyHeaders (HttpURLConnection httpConnection, HttpResponseMessage response
895963
} else {
896964
item_headers = response.Headers;
897965
}
898-
item_headers.TryAddWithoutValidation (key, headers [key]);
966+
967+
IEnumerable<string> values = headers [key];
968+
if (removeContentLength && String.Compare (ContentLengthHeaderName, key, StringComparison.OrdinalIgnoreCase) == 0) {
969+
removeContentLength = false;
970+
continue;
971+
}
972+
973+
if ((removeContentEncoding || setNewContentEncodingValue) && String.Compare (ContentEncodingHeaderName, key, StringComparison.OrdinalIgnoreCase) == 0) {
974+
if (removeContentEncoding) {
975+
removeContentEncoding = false;
976+
continue;
977+
}
978+
979+
setNewContentEncodingValue = false;
980+
values = contentState.NewContentEncodingHeaderValue!;
981+
}
982+
item_headers.TryAddWithoutValidation (key, values);
899983
}
984+
contentState.Reset ();
900985
}
901986

902987
/// <summary>
@@ -1006,19 +1091,24 @@ void AppendEncoding (string encoding, ref List <string>? list)
10061091
List <string>? accept_encoding = null;
10071092

10081093
decompress_here = false;
1009-
if ((AutomaticDecompression & DecompressionMethods.GZip) != 0) {
1010-
AppendEncoding (GZIP_ENCODING, ref accept_encoding);
1011-
decompress_here = true;
1012-
}
1013-
1014-
if ((AutomaticDecompression & DecompressionMethods.Deflate) != 0) {
1015-
AppendEncoding (DEFLATE_ENCODING, ref accept_encoding);
1016-
decompress_here = true;
1017-
}
1018-
10191094
if (AutomaticDecompression == DecompressionMethods.None) {
1020-
accept_encoding?.Clear ();
10211095
AppendEncoding (IDENTITY_ENCODING, ref accept_encoding); // Turns off compression for the Java client
1096+
} else {
1097+
if ((AutomaticDecompression & DecompressionMethods.GZip) != 0) {
1098+
AppendEncoding (GZIP_ENCODING, ref accept_encoding);
1099+
decompress_here = true;
1100+
}
1101+
1102+
if ((AutomaticDecompression & DecompressionMethods.Deflate) != 0) {
1103+
AppendEncoding (DEFLATE_ENCODING, ref accept_encoding);
1104+
decompress_here = true;
1105+
}
1106+
#if NETCOREAPP
1107+
if ((AutomaticDecompression & DecompressionMethods.Brotli) != 0) {
1108+
AppendEncoding (BROTLI_ENCODING, ref accept_encoding);
1109+
decompress_here = true;
1110+
}
1111+
#endif
10221112
}
10231113

10241114
if (accept_encoding?.Count > 0)

src/Xamarin.Android.Build.Tasks/Tests/Xamarin.ProjectTools/Resources/Base/BuildReleaseArm64XFormsDotNet.apkdesc

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,13 @@
1111
"Size": 7313
1212
},
1313
"assemblies/Java.Interop.dll": {
14-
"Size": 66563
14+
"Size": 66562
1515
},
1616
"assemblies/Mono.Android.dll": {
17-
"Size": 444617
17+
"Size": 444972
1818
},
1919
"assemblies/Mono.Android.Runtime.dll": {
20-
"Size": 5897
20+
"Size": 5822
2121
},
2222
"assemblies/mscorlib.dll": {
2323
"Size": 3866
@@ -64,6 +64,9 @@
6464
"assemblies/System.Drawing.Primitives.dll": {
6565
"Size": 12010
6666
},
67+
"assemblies/System.IO.Compression.Brotli.dll": {
68+
"Size": 11871
69+
},
6770
"assemblies/System.IO.Compression.dll": {
6871
"Size": 16858
6972
},
@@ -89,7 +92,7 @@
8992
"Size": 8154
9093
},
9194
"assemblies/System.Private.CoreLib.dll": {
92-
"Size": 814216
95+
"Size": 814322
9396
},
9497
"assemblies/System.Private.DataContractSerialization.dll": {
9598
"Size": 192370
@@ -131,7 +134,7 @@
131134
"Size": 1864
132135
},
133136
"assemblies/UnnamedProject.dll": {
134-
"Size": 5294
137+
"Size": 5286
135138
},
136139
"assemblies/Xamarin.AndroidX.Activity.dll": {
137140
"Size": 5867
@@ -206,7 +209,7 @@
206209
"Size": 93552
207210
},
208211
"lib/arm64-v8a/libmonodroid.so": {
209-
"Size": 379152
212+
"Size": 380656
210213
},
211214
"lib/arm64-v8a/libmonosgen-2.0.so": {
212215
"Size": 3106808
@@ -221,7 +224,7 @@
221224
"Size": 154904
222225
},
223226
"lib/arm64-v8a/libxamarin-app.so": {
224-
"Size": 333760
227+
"Size": 333840
225228
},
226229
"META-INF/android.support.design_material.version": {
227230
"Size": 12
@@ -335,13 +338,13 @@
335338
"Size": 1213
336339
},
337340
"META-INF/BNDLTOOL.SF": {
338-
"Size": 79326
341+
"Size": 79441
339342
},
340343
"META-INF/com.google.android.material_material.version": {
341344
"Size": 10
342345
},
343346
"META-INF/MANIFEST.MF": {
344-
"Size": 79199
347+
"Size": 79314
345348
},
346349
"META-INF/proguard/androidx-annotations.pro": {
347350
"Size": 339
@@ -1976,5 +1979,5 @@
19761979
"Size": 341228
19771980
}
19781981
},
1979-
"PackageSize": 7820036
1982+
"PackageSize": 7832413
19801983
}

0 commit comments

Comments
 (0)