I have a web application that is in two parts:
In trying to gradually convert the application to entirely use HTTPS, I thought it would be better to convert the back-end, first. I have built it so that I can toggle the ability to enable/disable HTTPS on the back-end.
I have set up the back-end to run:
The problem comes about when I attempt to run the entire application (front- and back-end) under localhost in a local development environment, but with an HTTP-to-HTTPS rewrite (redirect) rule. After that I receive CORS errors on the front-end.
In short, in my local development environment, I am attempting:
After reading on all things CORS for hours, I adjusted the web.config and the applicationHost.config based upon this blog post and this StackOverflow article in an attempt to capture the Request Origin header value. Here is what they look like.
My applicationHost.config contains this section:
<location path="Default Web Site">
<system.webServer>
<rewrite>
<allowedServerVariables>
<add name="CAPTURED_ORIGIN" />
<add name="RESPONSE_Access-Control-Allow-Origin" />
</allowedServerVariables>
</rewrite>
</system.webServer>
</location>
And here is my Web.config:
<?xml version="1.0" encoding="utf-8"?>
<!--
This configuration file is required if iisnode is used to run node processes behind
IIS or IIS Express. For more information, visit:
https://github.com/tjanczuk/iisnode/blob/master/src/samples/configuration/web.config
-->
<configuration>
<system.webServer>
<handlers>
<!-- Indicates that the server.js file is a node.js site to be handled by the iisnode module -->
<add name="iisnode" path="bin/www" verb="*" modules="iisnode" />
</handlers>
<rewrite>
<rules>
<rule name="Capture Origin Header">
<match url=".*" />
<conditions>
<add input="{HTTP_ORIGIN}" pattern=".+" />
</conditions>
<serverVariables>
<set name="CAPTURED_ORIGIN" value="{C:0}" />
</serverVariables>
<action type="None" />
</rule>
<!-- HTTP-to-HTTPS redirect -->
<rule name="Redirect to HTTPS" enabled="true" stopProcessing="true">
<match url="(.*)" ignoreCase="true" />
<conditions logicalGrouping="MatchAll" trackAllCaptures="true">
<add input="{HTTPS}" pattern="^off$" />
<add input="{HTTP_HOST}" pattern="([^/:]*?):[^/]*?" />
</conditions>
<action type="Redirect" url="https://{C:1}:3443/{R:0}" appendQueryString="true" redirectType="Temporary" />
</rule>
<!-- First we consider whether the incoming URL matches a physical file in the /public folder -->
<rule name="StaticContent">
<action type="Rewrite" url="public{REQUEST_URI}" />
</rule>
<!-- All other URLs are mapped to the node.js site entry point -->
<rule name="DynamicContent">
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="True" />
</conditions>
<action type="Rewrite" url="bin/www" />
</rule>
</rules>
<outboundRules>
<rule name="Set-Access-Control-Allow-Origin for known origins">
<match serverVariable="RESPONSE_Access-Control-Allow-Origin" pattern=".+" negate="true" />
<action type="Rewrite" value="{CAPTURED_ORIGIN}" />
</rule>
</outboundRules>
</rewrite>
<!-- Make sure error responses are left untouched -->
<httpErrors existingResponse="PassThrough" />
</system.webServer>
<appSettings>
<add key="HTTPS_ENABLED" value="true" />
</appSettings>
</configuration>
The NodeJS application is also set up to handle CORS from localhost:8000, localhost:3000, localhost:3443 (local https), and "null" (converted to a string). (More on that, later.)
But if I use this configuration, then I get the following error in the front-end:
XMLHttpRequest cannot load http://localhost:3000/foo/bar/ Response for preflight is invalid (redirect)
I suspect this is because IIS handled the redirect, but as a result it is handling the preflight check (HTTP OPTIONS) with an invalid response (redirect). However, according to this StackOverflow article, and the answer by @sideshowbarker, leads me to believe that the current version of Chrome, 59.0.3071.104, should be able to handle the HTTP redirect response from the CORS preflight OPTIONS request.
If I remove the server variables from the applicationHost.config and the HTTP-to-HTTPS rewrite and other rules and from the web.config, and then add code to allow the NodeJS application to handle the redirect to HTTPS, then the revised application looks like this:
Then it appears an unknown (NodeJS? IIS?) server error occurs because the request is cancelled:
You can see the cancellation in chrome://net-internals/#events even though the Origin in the request and the Access-Control-Allow-Origin in the response headers match:
There is no useful error message (even though one is being received by the client) which leads me to believe that it is IIS and not NodeJS that is cancelling the request and sending back no useful information.
I ended up adding a "null" entry to handle CORS when running under the NodeJS Lite Server (and not IIS as an experiment), but I need this to run under IIS/IISNode. However, there seems to be a problem with then IIS / IISNode / NodeJS combination.
I suspect that the Request Origin of "null" is most likely the result of a request, where the server performs a redirect, because you really have two requests: - The original request from the browser - The request that is the result of the redirect When the redirect occurs, I am hypothesizing that the origin in the redirected request is not that same as the original URL, and for the reasons stated in https://www.w3.org/TR/cors/#generic-cross-origin-request-algorithms, the Origin request header within the redirect is null.
However, that doesn't explain, why, when I let NodeJS handle the redirect that the Origin request and the Access-Control-Allow-Origin response header values are both null and the request still gets cancelled. :-/
Finally, if I eliminate any attempt at HTTP-to-HTTPS redirect, then the application works without issue in my development environment.
The response to the preflight OPTIONS
itself must always be a 2xx response—e.g., 200 or 204. The response to the preflight itself can never be a redirect—e.g., 302. The spec prohibits that.
But the response cited in the question shows a server responding with such a redirect; hence the error message cited. If a browser gets a 3xx response to a preflight OPTIONS request, the spec requires that the browser stop right there—to consider the preflight as failing.
So the only solution is to fix the server so it doesn’t give a redirect response for that OPTIONS
.
There’s a different scenario with redirects and preflights that’s described in the question at CORS request with Preflight and redirect: disallowed. Workarounds?. In that scenario, this is what occurs:
OPTIONS /documents/123 --> 204 (everything okay, please proceed)
GET /documents/123 --> 303 redirect to `/documents/abc`
That is, the response to the OPTIONS
request itself is a 2xx success, but the response to the subsequent actual request that the frontend code is sending is a 3xx.
For that case, the spec previously required browsers to stop and not allow the frontend code making the request to have access to the response, and browsers would respond with an error:
The request was redirected to '
http://localhost:3000/foo/bar/
', which is disallowed for cross-origin requests that require preflight.
Note that’s different than the error message cited for the scenario in the question, which is:
XMLHttpRequest cannot load
http://localhost:3000/foo/bar/
Response for preflight is invalid (redirect)
In that scenario (the one in the question), this is instead what occurs:
OPTIONS /documents/123 --> 307 redirect to `/documents/abc`
That is, the response to the OPTIONS
is 3xx. The spec still requires a preflight failure for that.
But that other disallowed for cross-origin requests that require preflight case no longer requires failure, and that error should no longer occur in browsers; the spec changed in August 2016 to no longer require it, and all browser engines subsequently updated to match that change.
Chrome was the last browser to implement the update, but it was fixed in the Chrome sources in December 2016, and the fix shipped in Chrome 57.