I'm developing a REST web service using Asp.Net Core 2.0 Web API technology (full .Net Framework 4.6.1) within Visual Studio 2017 (15.6.5).
Whenever I send a post request that exceeds 64 KB, it hangs and never completes. For example, a post request with size of 68'259 bytes fails whereas one with a size of 63'534 bytes does complete without any problems.
While the issue first started appearing when I tried to upload images (using MultipartFormDataContent
), the content of the request does not matter. I also tried to send the image both as a byte array and as a converted Base64 string inside of a property within another object instance.
When capturing traffic with Fiddler, the request never finishes. It never receives a response.
The controller method is never even called, as no breakpoint within it is ever hit. However, the Invoke method of our custom middleware is getting called, though it never runs past the line await this._next.Invoke(context)
.
public class VersioningMiddleware
{
private readonly RequestDelegate _next;
public VersioningMiddleware(RequestDelegate next)
{
this._next = next;
}
public async Task Invoke(HttpContext context)
{
...
await this._next.Invoke(context);
}
...
}
I used this tutorial for uploading images.
Upon trying to figure out (to no avail) why the webservice never returns a response, I downloaded the example solution to see if that one even works. And it did, though it uses .Net Standard instead of the full .Net Framework, so I created a new webservice project within that solution using the full .Net Framework, copied over the whole controller and also changed the target platform to x86 to match the configuration to my actual web service (we still have to query MS Access databases, which is why I’m stuck with a 32 bit application). This new version of the example web service also worked like a charm for uploading images bigger than 64 KB.
This didn’t make much sense to me, since I couldn’t see any significant differences.
App code sending the request (abbreviated in some parts for clarity’s sake):
public async Task UploadImageAsync(byte[] imageData, string fileName, object objectParameter, CancellationToken cancellationToken, params string[] pathComponents)
{
...
using (MemoryStream stream = new MemoryStream(imageData))
{
using (HttpContent fileStreamContent = new StreamContent(stream))
{
fileStreamContent.Headers.ContentDisposition = new ContentDispositionHeaderValue("form-data") { Name = "file", FileName = fileName };
fileStreamContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream");
using (MultipartFormDataContent formData = new MultipartFormDataContent())
{
formData.Add(fileStreamContent);
string apiPath = String.Join(Globals.PathSeparator, pathComponents);
if (objectParameter != null)
{
apiPath = apiPath + "?" + objectParameter.ToQueryString();
}
await this.SendPostAsync(apiPath, formData, cancellationToken).ConfigureAwait(false))
}
}
}
}
private Task<HttpResponseMessage> SendPostAsync(string path, HttpContent content, CancellationToken cancellationToken)
{
return this.SendRequestAsync(HttpAction.Post, path, content, cancellationToken);
}
private async Task<HttpResponseMessage> SendRequestAsync(HttpAction action, string path, HttpContent content, CancellationToken cancellationToken)
{
...
switch (action)
{
...
case HttpAction.Post:
response = await this._client.PostAsync(path, content, cancellationToken).ConfigureAwait(false);
break;
...
}
...
return response;
}
Web service controller code (also abbreviated):
[Route("api/[controller]")]
public class DocumentsController : ControllerBase
{
private readonly IDocumentsRepository _documents;
...
public DocumentsController(IDocumentsRepository documents, ...)
{
this._documents = documents;
...
}
...
[HttpPost(nameof(SaveImage))]
[Authorize(JwtBearerDefaults.AuthenticationScheme)]
public IActionResult SaveImage(IFormFile file, [FromQuery]ServiceReportIdentification documentInfo)
{
this._documents.SaveImage(file, documentInfo);
return this.CreatedAtAction(nameof(SaveImage), file);
}
...
}
The default IIS request size limit should be at 4 MB and most images I tried to upload were less than 1 MB, so this shouldn’t be an issue. However, I still tried manually setting all size limits I could find to see if I can override that hidden 64 KB size limit. No dice.
I even copied the httpCompression
tag from the .vs\config\applicationhost.config file of the example solution, which was the only real difference between that one and the one form my actual web service solution.
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<system.web>
<httpRuntime maxRequestLength="10240" />
</system.web>
<system.webServer>
<security>
<requestFiltering>
<requestLimits maxAllowedContentLength="10485760" />
</requestFiltering>
</security>
<handlers>
<add name="aspNetCore" path="*" verb="*" modules="AspNetCoreModule" resourceType="Unspecified" />
</handlers>
<aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="true" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="false" startupTimeLimit="3600" requestTimeout="23:00:00" />
<httpCompression>
<dynamicCompression>
<add mimeType="text/event-stream" enabled="false" />
</dynamicCompression>
</httpCompression>
</system.webServer>
<system.serviceModel>
<bindings>
<wsHttpBinding>
<binding name="ServicePlusApi" maxReceivedMessageSize="10485760" />
</wsHttpBinding>
</bindings>
</system.serviceModel>
</configuration>
When I created the web service solution, Asp.Net Core hasn’t reached version 2.0 yet, which is why there’s still a web.config file present in the Web API project. Since 2.0 (or at least that’s my assumption), the web.config file does no longer exist. Instead, the default configuration is stowed away inside the .vs\config\applicationhost.config file.
For that reason, I created a fresh solution and Web API project, added in the code from my web service as well as additional projects needed for referencing, essentially recreating the whole solution with the same codebase but latest project structure/definition.
Still, this didn’t fly.
I have no clue what is causing that weird 64 KB request size limit, which doesn’t even return an error, nor is there any record of the request in the IIS logs. It just swallows the request, never providing any sort of response (besides a client-side timeout).
Does anyone have any idea what could be the cause of this? I’ve scoured the internet for quite a while now and besides those manual size limit overrides, which don’t work in this case, I found nothing that helps.
Edit:
The above described behavior happens locally on a dev machine in debug mode using Visual Studio's IIS Express. The same applies when the web service is published in release mode and hosted by a standard IIS on a different machine.
Upon searching the IIS trace logs for more clues, I found the following event:
<Event xmlns="http://schemas.microsoft.com/win/2004/08/events/event">
<System>
...
</System>
<EventData>
<Data Name="ContextId">{8000006B-0006-FF00-B63F-84710C7967BB}</Data>
<Data Name="ModuleName">AspNetCoreModule</Data>
<Data Name="Notification">128</Data>
<Data Name="HttpStatus">400</Data>
<Data Name="HttpReason">Bad Request</Data>
<Data Name="HttpSubStatus">0</Data>
<Data Name="ErrorCode">2147952454</Data>
<Data Name="ConfigExceptionInfo"></Data>
</EventData>
<RenderingInfo Culture="de-CH">
<Opcode>MODULE_SET_RESPONSE_ERROR_STATUS</Opcode>
<Keywords>
<Keyword>RequestNotifications</Keyword>
</Keywords>
<freb:Description Data="Notification">EXECUTE_REQUEST_HANDLER</freb:Description>
<freb:Description Data="ErrorCode">Eine vorhandene Verbindung wurde vom Remotehost geschlossen.
(0x80072746)</freb:Description>
</RenderingInfo>
...
</Event>
The error description is in German and says: "An existing connection has been closed by the remote host."
A little further down, I found some response data which contains buffer data for the error page (which never shows due to the nature of the web service), containing the following error text: "The request could not be understood by the server due to malformed syntax."
Strangely, there's nor a request, nor a response entry within the normal IIS log. Just in the trace log.
After going through the process of elimination, I found the culprit of this request limitation.
System.Windows.Forms.RichTextBox
We needed it to extract plain text from RTF text, since our Xamarin app can't handle RTF text. Turns out an ASP.Net Core Web API web service doesn't like WinForms components.
When removing the instantiation of said RichTextBox, the image upload works like a charm, like it is supposed to.
Lesson learned: never use WinForms components within web services.
However, I still don't know why using a RichTextBox has this specific effect (requests over 64 KB don't get processed).
Does it trigger a hidden compatibility mode? Is it because of a missing UI thread?
If someone knows this, please let me know.