fix: retry session fetch with Authorization after redirect strips it #210
This commit is contained in:
+31
-6
@@ -446,12 +446,39 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async refreshSession(): Promise<void> {
|
/**
|
||||||
const sessionUrl = `${this.serverUrl}/.well-known/jmap`;
|
* Fetch the JMAP session, transparently handling servers that redirect
|
||||||
const response = await fetch(sessionUrl, {
|
* /.well-known/jmap to a canonical session URL (e.g. Stalwart → /jmap/session).
|
||||||
|
*
|
||||||
|
* Safari strips the Authorization header on cross-origin redirects even when
|
||||||
|
* the redirect destination is same-origin as the original request, and some
|
||||||
|
* reverse-auth proxies (e.g. Pangolin) admit the redirected request via a
|
||||||
|
* cookie without the Authorization header. Stalwart responds to an
|
||||||
|
* unauthenticated /jmap/session with 200 + empty accounts rather than 401,
|
||||||
|
* so the drop is silent and downstream parsing fails with "No mail account
|
||||||
|
* found in session".
|
||||||
|
*
|
||||||
|
* Detect that case (response.redirected, empty accounts, empty username)
|
||||||
|
* and retry directly against the final URL so we can re-send Authorization.
|
||||||
|
*/
|
||||||
|
private async fetchSessionResponse(): Promise<Response> {
|
||||||
|
const discoveryUrl = `${this.serverUrl}/.well-known/jmap`;
|
||||||
|
const response = await this.authenticatedFetch(discoveryUrl, { method: 'GET' });
|
||||||
|
if (!response.ok || !response.redirected) return response;
|
||||||
|
|
||||||
|
const peek = await response.clone().json().catch(() => null);
|
||||||
|
const hasAccounts = peek && Object.keys(peek.accounts || {}).length > 0;
|
||||||
|
const hasUsername = typeof peek?.username === 'string' && peek.username.length > 0;
|
||||||
|
if (hasAccounts || hasUsername) return response;
|
||||||
|
|
||||||
|
return fetch(response.url, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: { 'Authorization': this.authHeader },
|
headers: { 'Authorization': this.authHeader },
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async refreshSession(): Promise<void> {
|
||||||
|
const response = await this.fetchSessionResponse();
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Session refresh failed: ${response.status}`);
|
throw new Error(`Session refresh failed: ${response.status}`);
|
||||||
@@ -470,9 +497,7 @@ export class JMAPClient implements IJMAPClient {
|
|||||||
const sessionUrl = `${this.serverUrl}/.well-known/jmap`;
|
const sessionUrl = `${this.serverUrl}/.well-known/jmap`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sessionResponse = await this.authenticatedFetch(sessionUrl, {
|
const sessionResponse = await this.fetchSessionResponse();
|
||||||
method: 'GET',
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!sessionResponse.ok) {
|
if (!sessionResponse.ok) {
|
||||||
if (sessionResponse.status === 401) {
|
if (sessionResponse.status === 401) {
|
||||||
|
|||||||
Reference in New Issue
Block a user