Spec doesn't care - and actually shouldn't care about that. Your server shouldn't care either - and just resolve the CORS part of its response as soon as possible.
See, when user agent does Preflight request, it (by spec) is expected to only care whether it's successful or not.
If it is successful, the result is usually placed into a so called CORS-preflight Cache to be employed in subsequent requests with matching methods, urls and headers.
But if it is not, these cache entries will be cleared - regardless of the reason. Oh, and main request won't be processed too, of course.
Now, the spec goes into much details describing what exactly is considered success:
- response passes CORS Check
- response status is ok status (any status in the range 200 to 299, inclusive)
Access-Control-Allow-Methods
and Access-Control-Allow-Headers
headers are there,
their values match expectations...
... and there's a lot more there. But the point is, failure at any step always results in "...then return a network error" result.
And this is how network error looks like:
A network error is a response whose status is always 0, status message
is always the empty byte sequence, header list is always empty, and
body is always null.
Did I say spec doesn't care?
It might be interesting to check how Chromium implements that spec:
std::unique_ptr<PreflightResult> CreatePreflightResult(/* args skipped */) {
const int response_code = head.headers ? head.headers->response_code() : 0;
*detected_error_status = CheckPreflightAccess(
final_url, response_code,
GetHeaderString(head.headers, header_names::kAccessControlAllowOrigin),
GetHeaderString(head.headers,
header_names::kAccessControlAllowCredentials),
original_request.credentials_mode,
tainted ? url::Origin() : *original_request.request_initiator);
if (*detected_error_status)
return nullptr;
base::Optional<mojom::CorsError> error;
error = CheckPreflight(response_code);
if (error) {
*detected_error_status = CorsErrorStatus(*error);
return nullptr;
}
if (original_request.is_external_request) {
*detected_error_status = CheckExternalPreflight(GetHeaderString(
head.headers, header_names::kAccessControlAllowExternal));
if (*detected_error_status)
return nullptr;
}
auto result = PreflightResult::Create(
original_request.credentials_mode,
GetHeaderString(head.headers, header_names::kAccessControlAllowMethods),
GetHeaderString(head.headers, header_names::kAccessControlAllowHeaders),
GetHeaderString(head.headers, header_names::kAccessControlMaxAge),
&error);
if (error)
*detected_error_status = CorsErrorStatus(*error);
return result;
}
As you can see, this...
return nullptr;
... is a leitmotif of the function: whenever a check is failed, the function immediately returns null pointer. True, detected_error_status
is filled as well, but it seems to be there mostly for debugging purposes (i.e., to see at which step check had failed - and design the console warning accordingly).