I'm trying to hit my own endpoint on a subdomain controlled by Nginx, I expect the request to fail and return a JSON payload like this:
{
    "hasError": true,
    "data": null,
    "error": {
        "statusCode": 401,
        "statusDescription": null,
        "message": "Could not find Session Reference in Request Headers"
    }
}
When I make this request in a browser, it returns a 401 with this in the network tools (Brave Browser):
And this error in the console:
Access to fetch at 'https://services.mfwebdev.net/api/authentication/validate-session' from origin 'https://mfwebdev.net' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
When I hit the URL in question in the browser, I see the correct JSON response, if I hit the URL in a REST client like insomnia, I can see the JSON response.
The headers that browser is sending are:
:authority: services.mfwebdev.net
:method: GET
:path: /api/authentication/validate-session
:scheme: https
accept: application/json, text/plain, */*
accept-encoding: gzip, deflate, br
accept-language: en-GB,en-US;q=0.9,en;q=0.8
origin: https://mfwebdev.net
referer: https://mfwebdev.net/
sec-fetch-dest: empty
sec-fetch-mode: cors
sec-fetch-site: same-site
sec-gpc: 1
user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.110 Safari/537.36
I've actually used these headers in the REST client as well and I can still see the correct JSON result.
The request (in code) is as follows (Using Angular):
import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { ApiResponse } from 'src/app/api/types/response/api-response.class';
import { ISessionApiService } from 'src/app/api/types/session/session-api-service.interface';
import { SessionResponse } from 'src/app/api/types/session/session-response.class';
import { environment } from '../../../environments/environment';
@Injectable()
export class SessionApiService implements ISessionApiService {
    private readonly _http: HttpClient;
    constructor(http: HttpClient) {
        this._http = http;
    }
    public createSession(): Observable<ApiResponse<SessionResponse>> {
        return this._http.post<ApiResponse<SessionResponse>>(`${environment.servicesApiUrl}/authentication/authorise`, {
            reference: environment.applicationReference,
            applicationName: environment.applicationName,
            referrer: environment.applicationReferrer
         });
    }
    public validateSession(): Observable<ApiResponse<boolean>> {
        return this._http.get<ApiResponse<boolean>>(`${environment.servicesApiUrl}/authentication/validate-session`);
    }
}
Could someone please help, I'm at a complete loss here.
EDIT!! For anyone using NginX who may come across this problem. The issue was in my nginx.conf file. I am leaving an example of my(now working) server-side configuration.
The reason it wasn't working was because I was not bothering to actually handle the request if an OPTIONS request came through.
I now handle every request type (or will) and append the ACCESS-CONTROL-ALLOW-ORIGIN header to the request.
user root;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {}
http {
    include        /etc/nginx/proxy.conf;
    limit_req_zone $binary_remote_addr zone=one:10m rate=5r/s;
    server_tokens  off;
    sendfile on;
    # Adjust keepalive_timeout to the lowest possible value that makes sense
    # for your use case.
    keepalive_timeout   1000;
    client_body_timeout 1000;
    client_header_timeout 10;
    send_timeout 10;
    upstream upstreamExample{
        server 127.0.0.1:5001;
    }
    server {
        listen                    443 ssl http2;
        listen                    [::]:443 ssl http2;
        server_name               example.net *.example.net;
        ssl_certificate           /etc/letsencrypt/live/example.net/cert.pem;
        ssl_certificate_key       /etc/letsencrypt/live/example.net/privkey.pem;
        ssl_session_timeout       1d;
        ssl_protocols             TLSv1.2 TLSv1.3;
        ssl_prefer_server_ciphers off;
        ssl_ciphers               ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
        ssl_session_cache         shared:SSL:10m;
        ssl_session_tickets       off;
        ssl_stapling              off;
        location / {
            if ($request_method = 'OPTIONS') {
                add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
                add_header 'Access-Control-Max-Age' 1728000;
                add_header 'Content-Type' 'text/plain; charset=utf-8';
                add_header 'Content-Length' 0;
                add_header 'Access-Control-Allow-Origin' '*';
                add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
                add_header 'Access-Control-Allow-Headers' '*';
                return 204;
            }
            if ($request_method = 'POST') {
                add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
                add_header 'Access-Control-Max-Age' 1728000;
                add_header 'Access-Control-Allow-Origin' '*' always;
                add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
                add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
                add_header 'Access-Control-Allow-Headers' '*';
            }
            if ($request_method = 'GET') {
                add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
                add_header 'Access-Control-Max-Age' 1728000;
                add_header 'Access-Control-Allow-Origin' '*' always;
                add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
                add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
                add_header 'Access-Control-Allow-Headers' '*';
            }
            if ($request_method = 'DELETE') {
                add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
                add_header 'Access-Control-Max-Age' 1728000;
                add_header 'Access-Control-Allow-Origin' '*' always;
                add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
                add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
                add_header 'Access-Control-Allow-Headers' '*';
            }
            proxy_pass https://upstreamExample;
            limit_req  zone=one burst=10 nodelay;
        }
    }
}

 
    