I'm using a traditional WebAPI controller:
[Route("api/results/{query}")]
[AcceptVerbs("GET")]
public HttpResponseMessage GetQueryResults(string query)
{
var userAgent = Request.Headers.UserAgent;
var result = _fooService.GetResults(GetUsername(), query);
var response = Request.CreateResponse(HttpStatusCode.OK, result);
return response;
}
GetResults returns an array of elements that looks like this:
[{
"resultId":2039016,
"text":null,
"dateCreated":"2020-09-10T02:24:36.003",
"targetPlatform":"FooBar"
}]
On most browsers, this works fine. My user agent header looks like this:
{Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36}
However when I debug from Chrome using the device toolbar, or when I visit my site from Safari on my iPhone, my user agent changes. From Chrome's device toolbar (the mobile simulator), it looks something like this:
{Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Mobile Safari/537.36}
And in this case, CheckInvalidPathChars is invoked and fails against the JSON:
"type": "System.ArgumentException",
"message": "Illegal characters in path.",
"stackTrace": " at System.IO.Path.CheckInvalidPathChars(String path, Boolean checkAdditional)
at System.IO.Path.GetExtension(String path)
at System.Web.WebPages.DefaultDisplayMode.TransformPath(String virtualPath, String suffix)
at System.Web.WebPages.DefaultDisplayMode.GetDisplayInfo(HttpContextBase httpContext, String virtualPath, Func`2 virtualPathExists)
at System.Web.WebPages.DisplayModeProvider.GetDisplayInfoForVirtualPath(String virtualPath, HttpContextBase httpContext, Func`2 virtualPathExists, IDisplayMode currentDisplayMode, Boolean requireConsistentDisplayMode)
at System.Web.WebPages.WebPageRoute.GetRouteLevelMatch(String pathValue, String[] supportedExtensions, Func`2 virtualPathExists, HttpContextBase context, DisplayModeProvider displayModeProvider)
at System.Web.WebPages.WebPageRoute.MatchRequest(String pathValue, String[] supportedExtensions, Func`2 virtualPathExists, HttpContextBase context, DisplayModeProvider displayModes)
at System.Web.WebPages.WebPageRoute.DoPostResolveRequestCache(HttpContextBase context)
at System.Web.WebPages.WebPageHttpModule.OnApplicationPostResolveRequestCache(Object sender, EventArgs e)
at System.Web.HttpApplication.SyncEventExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStepImpl(IExecutionStep step)
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)"
I can reproduce this manually by attempting to parse the serialized JSON as a file path:
try
{
var isValid = System.IO.Path.GetExtension(jsonString);
}
catch (Exception e)
{
throw e;
}
But of course, attempting to parse a serialized JSON object as a file will throw errors. Why is ASP.NET modifying parsing behavior based on user agent headers?
Can I somehow override the inbound header from the Request to coerce the framework towards functional behavior?
To be clear - I can invoke the controller from Chrome (standard) with no issues. When I invoke the controller with the same exact request from Chrome (with devtools open and the mobile simulator active), the exception is thrown. Likewise, when I invoke it with the same exact request from Safari on iPhone, the exception is thrown. The User Agent header is the independent variable in these cases - so it must follow that this header is somehow causing a different execution path to be invoked. Right?
The exception is thrown after all Controller logic has executed - the response is returned by the controller when it's thrown.
As @Dipen Shah pointed out, the solution is in another post.
Why is ASP.NET modifying parsing behavior based on user agent headers?
There is a WebPageHttpModule registered automatically in your ASP.NET application (reference). Internally, WebPageHttpModule tries to route request based on display mode.
// NOTE: Excluded unnecessary code
public sealed class DisplayModeProvider
{
public static readonly string MobileDisplayModeId = "Mobile";
private readonly List<IDisplayMode> _displayModes = new List<IDisplayMode>()
{
(IDisplayMode) new DefaultDisplayMode(DisplayModeProvider.MobileDisplayModeId)
{
ContextCondition = (Func<HttpContextBase, bool>) (context => context.GetOverriddenBrowser().IsMobileDevice)
},
(IDisplayMode) new DefaultDisplayMode()
};
public IList<IDisplayMode> Modes
{
get
{
return (IList<IDisplayMode>) this._displayModes;
}
}
}
You can see string Mobile there and check condition against IsMobileDevice (reference). That's why the solution in aforementioned post makes sense. Once you know how it works internally, you can do the following hack as well.
public static void RegisterDisplayModes()
{
var displayModes = DisplayModeProvider.Instance.Modes;
var mobileDisplayMode = displayModes.FirstOrDefault(d => d.DisplayModeId == DisplayModeProvider.MobileDisplayModeId);
if (mobileDisplayMode != null)
{
displayModes.Remove(mobileDisplayMode);
}
}
Note, this is based on decompiled System.Web.WebPages.dll located at C:\Program Files (x86)\Microsoft ASP.NET\ASP.NET Web Pages\v2.0\Assemblies\.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With