-
Notifications
You must be signed in to change notification settings - Fork 483
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
Authorization
header occurs even ifglobal.headers.Authorization
is configured on the client. - Manually calling the REST endpoint with only the
apikey
header (and noAuthorization
header) 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 forSUBSCRIBED
beforech.send(...)
. - Call
supabase.realtime.setAuth(<service_role_jwt>)
server-side so the fallback’sAuthorization
is not empty.
Expected behavior
- If no session/access token is present, the REST fallback should omit the
Authorization
header (or allow overriding it), rather than sending an emptyAuthorization:
line. - Alternatively, the fallback could honor
global.headers.Authorization
when 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.persistSession
is false, so there is intentionally no user session.- Setting
global.headers.Authorization
does 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_token
is falsy during REST fallback, do not setAuthorization
at all (or userealtime.getAuth()
/global.headers
as a fallback). - In Realtime, return 401/400 instead of 500 for empty/invalid Authorization headers.