68 lines
2.9 KiB
JavaScript
68 lines
2.9 KiB
JavaScript
// The single network boundary of the whole project.
|
|
//
|
|
// NOTHING else in Tofu calls fetch: the WebDAV client, the diagnostics and the
|
|
// planning store all go through this function. One file to read to know how the
|
|
// app talks to the outside, how it authenticates, and what it does when that
|
|
// fails.
|
|
|
|
import { describeWebdavFailure } from '#tofu/tools/webdav/describeWebdavFailure.js'
|
|
|
|
// fetch was rejected before any response existed: there is no HTTP status at
|
|
// all, and 0 is the usual way to say so.
|
|
const NO_STATUS = 0
|
|
|
|
// Sends one WebDAV request. Never throws.
|
|
// -> { ok: true, status, text, responseHeaders }
|
|
// -> { ok: false, failure: { kind, message, hint, status } }
|
|
export async function webdavRequest({ url, method, headers = {}, body, authorization }) {
|
|
const requestHeaders = { ...headers }
|
|
if (authorization) requestHeaders.Authorization = authorization
|
|
|
|
try {
|
|
// `credentials: 'omit'` is a deliberate choice, and it is the reason a page
|
|
// served from another origin can reach a Nextcloud at all.
|
|
//
|
|
// We build the `Authorization: Basic …` header ourselves, so the request
|
|
// stays "non credentialed" in the CORS sense. The browser then accepts
|
|
// `Access-Control-Allow-Origin: *` from the server. With
|
|
// `credentials: 'include'` the very same request would become credentialed:
|
|
// the wildcard would be rejected, and the server would have to echo our
|
|
// exact origin AND send `Access-Control-Allow-Credentials: true` — which a
|
|
// shared Nextcloud almost never does.
|
|
//
|
|
// The trade-off is explicit: the password stays in memory and is sent by
|
|
// us on every request, instead of being held by the browser in a session
|
|
// cookie. Nothing is persisted anywhere.
|
|
const response = await fetch(url, {
|
|
method,
|
|
mode: 'cors',
|
|
credentials: 'omit',
|
|
headers: requestHeaders,
|
|
body
|
|
})
|
|
|
|
// Read the body before branching: a 4xx from Nextcloud carries an XML
|
|
// explanation that is worth surfacing, and a consumed body cannot be read
|
|
// twice.
|
|
const text = await response.text()
|
|
|
|
if (!response.ok) {
|
|
const failure = describeWebdavFailure({ error: null, response, method })
|
|
return { ok: false, failure: { ...failure, status: response.status } }
|
|
}
|
|
|
|
return {
|
|
ok: true,
|
|
status: response.status,
|
|
text,
|
|
responseHeaders: Object.fromEntries(response.headers)
|
|
}
|
|
} catch (error) {
|
|
// A rejected fetch towards a third-party origin is almost always CORS. The
|
|
// browser hides which one it was on purpose: a blocked request and an
|
|
// unreachable server produce the exact same TypeError, with no detail. So
|
|
// describeWebdavFailure says both, honestly, instead of asserting one.
|
|
const failure = describeWebdavFailure({ error, response: null, method })
|
|
return { ok: false, failure: { ...failure, status: NO_STATUS } }
|
|
}
|
|
}
|