-
Couldn't load subscription status.
- Fork 501
Description
Bug report
- I confirm this is a bug with Supabase, not with my own application.
- I confirm I have searched the [Docs](https://docs.supabase.com), GitHub [Discussions](https://github.com/supabase/supabase/discussions), and [Discord](https://discord.supabase.com).
Describe the bug
When using @supabase/supabase-js to broadcast to Realtime without subscribing to the channel first, the client falls back to the REST endpoint /realtime/v1/api/broadcast. In this REST fallback, if there is no user session/access token, the library sends an empty Authorization header (exactly Authorization: \r\n, with no value at all). This results in a 500 Internal Server Error from the Realtime endpoint.
Notes:
- The empty
Authorizationheader occurs even ifglobal.headers.Authorizationis configured on the client. - Manually calling the REST endpoint with only the
apikeyheader (and noAuthorizationheader) works and returns 202 Accepted. - Calling
client.realtime.setAuth(<token>)(server-side) or using WebSocket send aftersubscribe()also avoids the issue.
It looks like the REST fallback builds/forces an Authorization header from the current auth session, and if none exists, it still emits an empty header instead of omitting it (or honoring global.headers).
To Reproduce
Minimal server-side repro (no user session, Service Role client):
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.SUPABASE_SERVICE_ROLE_KEY!, // server-side only
{
auth: { autoRefreshToken: false, persistSession: false },
// Setting global headers does not help for the fallback:
global: {
headers: {
apikey: process.env.SUPABASE_SERVICE_ROLE_KEY!,
Authorization: `Bearer ${process.env.SUPABASE_SERVICE_ROLE_KEY!}`,
},
},
}
)
// No ch.subscribe() here → triggers REST fallback:
const ch = supabase.channel(`channel:${'some-channel-id'}`, {
config: { private: true },
})
const ok = await ch.send({
type: 'broadcast',
event: 'new-message',
payload: { hello: 'world' },
})
// Result: HTTP 500 from /realtime/v1/api/broadcast.
// Request contains an EMPTY Authorization header.
console.log('send ok?', ok)Observed request headers (captured with Wireshark; redacted):
POST /realtime/v1/api/broadcast
apikey: <service_role_jwt>
Authorization:
Content-Type: application/json
If I instead call the REST endpoint manually with no Authorization header:
await fetch(`${process.env.NEXT_PUBLIC_SUPABASE_URL}/realtime/v1/api/broadcast`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
apikey: process.env.SUPABASE_SERVICE_ROLE_KEY!,
},
body: JSON.stringify({
messages: [{
topic: `channel:${'some-channel-id'}`,
event: 'new-message',
payload: { hello: 'world' },
private: true,
}],
}),
})→ Returns 202 Accepted and clients receive the broadcast as expected.
Workarounds that also work:
- Use WebSocket send by calling
ch.subscribe()and waiting forSUBSCRIBEDbeforech.send(...). - Call
supabase.realtime.setAuth(<service_role_jwt>)server-side so the fallback’sAuthorizationis not empty.
Expected behavior
- If no session/access token is present, the REST fallback should omit the
Authorizationheader (or allow overriding it), rather than sending an emptyAuthorization:line. - Alternatively, the fallback could honor
global.headers.Authorizationwhen no session exists. - Additionally, the Realtime REST endpoint should not return 500 for an empty/invalid Authorization header; a 401/400 would be more appropriate.
Screenshots
N/A — network capture shows the header exactly as Authorization: \r\n (no value) when using channel.send() without subscribe().
System information
- OS: Windows 11 Home
- Browser (if applies): N/A (server-side usage)
- Version of supabase-js: ^2.57.4
- Version of Node.js: 22.x
Additional context
- Client is created with Service Role key on the server only (never in the browser).
auth.persistSessionis false, so there is intentionally no user session.- Setting
global.headers.Authorizationdoes not affect the REST fallback. - This is easy to hit in practice: calling
channel.send()beforesubscribe()is a common mistake/edge case, and receiving a 500 due to an empty header is confusing. - Note: The HTTP 500 status was only visible via a network capture (Wireshark);
channel.send(...)merely returned"error"without status code or body, which significantly complicated debugging.
Suggested fix ideas:
- In
@supabase/supabase-js, ifaccess_tokenis falsy during REST fallback, do not setAuthorizationat all (or userealtime.getAuth()/global.headersas a fallback). - In Realtime, return 401/400 instead of 500 for empty/invalid Authorization headers.