Problems when using ServerSide Events and nginx

We found a problem when using ServerSide Events and nginx to access out host.

We have a c# service running under

http://localhost:5172/service

Our SSE endpoint is at

http://localhost:5172/service/sse

Our nginx configuration has a section for the SSE endpoint. It will make this translation

outside: https://our.full.domain/sse -> inside: http://localhost:5172/service/sse

This is the full nginx configuration section

location /sse {
            proxy_pass                 http://localhost:5172/service/sse;
            proxy_http_version         1.1;
            proxy_set_header           Upgrade $http_upgrade;
            proxy_set_header           Host $host;
            proxy_cache_bypass         $http_upgrade;
            proxy_set_header           X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header           X-Forwarded-Proto $scheme;
                                           
            proxy_set_header           Connection '';
            proxy_buffering            off;                                                                        
            proxy_cache                off;        
            chunked_transfer_encoding  off;
}              

The problem is:

We’re using nginx for security and for its ability to provide SSL.

When the subscription event happens, the c# logic for ServerEventsFeature seems to run this code

subscription.ConnectArgs = new Dictionary<string, string>(subscription.ConnectArgs) {
{“id”, subscriptionId },
{“unRegisterUrl”, unRegisterUrl},
{“heartbeatUrl”, heartbeatUrl},
{“updateSubscriberUrl”, req.ResolveAbsoluteUrl("~/".CombineWith(feature.SubscribersPath, subscriptionId)) },
{“heartbeatIntervalMs”, ((long)feature.HeartbeatInterval.TotalMilliseconds).ToString(CultureInfo.InvariantCulture) },
{“idleTimeoutMs”, ((long)feature.IdleTimeout.TotalMilliseconds).ToString(CultureInfo.InvariantCulture)}
};

At this point, the updateSubscriberUrl endpoint is calculated like this

{“updateSubscriberUrl”, req.ResolveAbsoluteUrl("~/".CombineWith(feature.SubscribersPath, subscriptionId)) },

Which causes the URL to be formatted as:

https://our.full.domain/service/sse/event-subscribers

Instead of

https://our.full.domain/sse/event-subscribers

Due to this, the nginx server receives a request on

/service/sse

which results in an error on our the typescript client app

And sometimes we get failed URLs in the browser

Fetch failed loading: POST “https://our.full.domain/sse/json/reply/UpdateEventSubscriber”.

Question: Is there a way we can override this behavior?

Is there a way we can setup the url as we do for the other events?

Why is this url being treated in a different way, rather than having a property named UpdateSubscriberUrl, like we have for the other events?

We attempted to override using

var sseBasePath = “/sse”;
se.StreamPath = $"{sseBasePath}/event-stream";
se.HeartbeatPath = $"{sseBasePath}/event-heartbeat";
se.SubscribersPath = $"{sseBasePath}/event-subscribers";
se.UnRegisterPath = $"{sseBasePath}/event-unregister";

But that did not work either.

Any help will be greatly appreciated.

Please provide the raw HTTP Request Header the client is making so I can see the actual path it’s trying to use.

Can you confirm the ServerEventsFeature.OnCreated callback is definitely getting invoked? If not the StreamPath doesn’t match the request that the ServiceStack App receives.

On the C# service side, the OnCreated event is definitely called, we also can send messages through the channel, the problem seems to be only for event-heartbeat and UpdateEventSubscriber

For event-heartbeat:

Request URL: https://local-service.refind.email/venue/sse/event-heartbeat?id=5MrGvw4yN74vtsju2UCi

Request Headers:

POST /venue/sse/event-heartbeat?id=5MrGvw4yN74vtsju2UCi HTTP/1.1
Host: local-service.refind.email
Connection: keep-alive
Content-Length: 0
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36
content-type: text/plain
Accept: /
Origin: https://local-prod.refind.email
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://local-prod.refind.email/app/main/recommend
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cookie: ss-pid=ZjMDVE6clNN5LzcfcTwd; ss-id=BrUXL9NvkFZW2dqFOPR3

For UpdateEventSubscribers:

Request URL: https://local-service.refind.email/sse/json/reply/UpdateEventSubscriber

Request Headers:

POST /sse/json/reply/UpdateEventSubscriber HTTP/1.1
Host: ocal-service.refind.email
Connection: keep-alive
Content-Length: 101
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_5) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36
content-type: application/json
Accept: /
Origin: https://local-prod.refind.email
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://local-prod.refind.email/app/main/recommend
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Cookie: ss-pid=ZjMDVE6clNN5LzcfcTwd; ss-id=BrUXL9NvkFZW2dqFOPR3

a guy on my team, juan mosquele generated this and he is having an issue signing up for the forum. his email is juan@commondesk.com

I’ve run a manual sync, can you get juan to try signing in again please.

Why are trying to configure different URLs for different functionality? i.e. why aren’t you just doing the normal URL mapping of an external domain to an internal port?

https://our.full.domain -> http://localhost:5172

The issue is in resolving the correct external URL, if you had a single URL for your entire App you could specify the external URL ServiceStack should use for its URLs by configuring Config.WebHostUrl, e.g:

SetConfig(new HostConfig {
    WebHostUrl = HostingEnvironment.IsDevelopment()
        ? "http://localhost:5172/service"
        : "https://our.full.domain/sse"
})

But that’s not going to work if you have different mappings for different parts of ServiceStack, in which case you can override ResolveAbsoluteUrl() in your AppHost and return the correct external URL as per your custom App requirements:

public override string ResolveAbsoluteUrl(string virtualPath, IRequest httpReq) 
    => ...

If you just need to change the SSE URLs you can update them in your OnConnect callback, e.g:

Plugins.Add(new ServerEventsFeature { 
    OnConnect = (sub,args) => {
      args["unRegisterUrl"] = ...;
      args["heartbeatUrl"] = ...;
      args["updateSubscriberUrl"] = ...;
    }
});
1 Like