4

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.

Frédéric
  • 9,364
  • 3
  • 62
  • 112
BLAZORLOVER
  • 1,971
  • 2
  • 17
  • 27
  • My bet is your code that constructs url in JavaScript is broken in different ways in different browsers... has nothing to do with user agent... Showing that code or at least urls would help someone to actually point exact problems. – Alexei Levenkov Sep 10 '20 at 03:01
  • @AlexeiLevenkov what url are you referring to? The error is thrown outside of the controller - after the body of the controller has executed. The url is very simple - `api/results/foo` – BLAZORLOVER Sep 10 '20 at 03:03
  • 1
    I see... Somehow your site configured to reply with HTML to those browsers and JSON is somehow used as view name... Check if "accept" header is there in both requests... (You will have much better luck narrowing it down if you use Fiddler or some other similar tool). If your site does not need to render HTML make sure nothing related to web pages is registered... – Alexei Levenkov Sep 10 '20 at 03:15
  • 2
    @BLAZORLOVER looks like an already solved issue: https://stackoverflow.com/questions/33694842/illegal-characters-in-path-depending-on-user-agent – Dipen Shah Oct 26 '20 at 04:05
  • @DipenShah indeed, thanks for the link. – BLAZORLOVER Oct 26 '20 at 16:37
  • @BLAZORLOVER if you don't mind could you please close the question in that case? – Dipen Shah Oct 26 '20 at 16:37
  • 1
    @DipenShah I can't because the bounty is live which I can't remove either. – BLAZORLOVER Oct 26 '20 at 17:55

2 Answers2

2

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\.

Han Zhao
  • 1,932
  • 7
  • 10
0

I used Asp.net Core kindly find the code below and screenshot assuming i understood your question perfectly.

 [Route("api/results/{query}")]
    [HttpGet]
    public IActionResult GetQueryResults(string query)
    {
        var userAgent = Request.Headers["User-Agent"].ToString();
        var result = @"[{\'resultId\':2039016,\'text\':null,\'dateCreated\':\'2020-09-10T02:24:36.003\',\'targetPlatform\':\'FooBar\'}]";
        // _fooService.GetResults(GetUsername(), query);
        return Ok(new { result = result, userAgent = userAgent });
    }
  • This is the image screenshot from vscode

enter image description here

  • This is the screenshot using chrome mobile responsive

enter image description here

  • This is the screenshot using chrome web or desktop view

enter image description here