diff --git a/libraries/fetch-utils/index.js b/libraries/fetch-utils/index.js index 5f30ac25b3..f164dfaa9e 100644 --- a/libraries/fetch-utils/index.js +++ b/libraries/fetch-utils/index.js @@ -80,6 +80,35 @@ async function fetchNothing(url, opts = {}) { return response } +/** + * Make a request and extract the redirect from the response. + * + * @param {string | URL} url - request URL + * @param {object} opts - fetch options + * @return {Promise} + * @throws {RequestFailedError} if the response has a non redirect status code or missing Location header + */ +async function fetchRedirect(url, opts = {}) { + const { fetchOpts } = parseOpts(opts) + fetchOpts.redirect = 'manual' + const response = await performRequest(url, fetchOpts) + if (response.status < 300 || response.status >= 400) { + const body = await maybeGetResponseBody(response) + throw new RequestFailedError(url, opts, response, body) + } + const location = response.headers.get('Location') + if (!location) { + const body = await maybeGetResponseBody(response) + throw new RequestFailedError(url, opts, response, body).withCause( + new OError('missing Location response header on 3xx response', { + headers: Object.fromEntries(response.headers.entries()), + }) + ) + } + await discardResponseBody(response) + return location +} + /** * Make a request and return a string. * @@ -222,6 +251,7 @@ module.exports = { fetchStream, fetchStreamWithResponse, fetchNothing, + fetchRedirect, fetchString, fetchStringWithResponse, RequestFailedError, diff --git a/libraries/fetch-utils/test/unit/FetchUtilsTests.js b/libraries/fetch-utils/test/unit/FetchUtilsTests.js index 8ab21921a9..fae8365359 100644 --- a/libraries/fetch-utils/test/unit/FetchUtilsTests.js +++ b/libraries/fetch-utils/test/unit/FetchUtilsTests.js @@ -7,6 +7,7 @@ const { fetchJson, fetchStream, fetchNothing, + fetchRedirect, fetchString, RequestFailedError, } = require('../..') @@ -205,6 +206,36 @@ describe('fetch-utils', function () { await expectRequestAborted(this.server.lastReq) }) }) + + describe('fetchRedirect', function () { + it('returns the immediate redirect', async function () { + const body = await fetchRedirect(this.url('/redirect/1')) + expect(body).to.equal(this.url('/redirect/2')) + }) + + it('rejects status 200', async function () { + await expect(fetchRedirect(this.url('/hello'))).to.be.rejectedWith( + RequestFailedError + ) + await expectRequestAborted(this.server.lastReq) + }) + + it('rejects empty redirect', async function () { + await expect(fetchRedirect(this.url('/redirect/empty-location'))) + .to.be.rejectedWith(RequestFailedError) + .and.eventually.have.property('cause') + .and.to.have.property('message') + .to.equal('missing Location response header on 3xx response') + await expectRequestAborted(this.server.lastReq) + }) + + it('handles errors', async function () { + await expect(fetchRedirect(this.url('/500'))).to.be.rejectedWith( + RequestFailedError + ) + await expectRequestAborted(this.server.lastReq) + }) + }) }) async function streamToString(stream) { diff --git a/libraries/fetch-utils/test/unit/helpers/TestServer.js b/libraries/fetch-utils/test/unit/helpers/TestServer.js index 5a0eae03e9..0fb69d5454 100644 --- a/libraries/fetch-utils/test/unit/helpers/TestServer.js +++ b/libraries/fetch-utils/test/unit/helpers/TestServer.js @@ -73,6 +73,18 @@ class TestServer { // Never returns this.app.post('/hang', (req, res) => {}) + + // Redirect + + this.app.get('/redirect/1', (req, res) => { + res.redirect('/redirect/2') + }) + this.app.get('/redirect/2', (req, res) => { + res.send('body after redirect') + }) + this.app.get('/redirect/empty-location', (req, res) => { + res.sendStatus(302) + }) } start(port) { diff --git a/package-lock.json b/package-lock.json index c3bdc4f414..342495dadc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -41413,6 +41413,7 @@ "version": "0.0.1", "license": "MIT", "dependencies": { + "@overleaf/fetch-utils": "*", "aws-sdk": "^2.1174.0", "axios": "^0.21.2", "body-parser": "^1.19.2", @@ -67341,6 +67342,7 @@ "@babel/preset-env": "^7.23.2", "@babel/preset-react": "^7.22.15", "@babel/register": "^7.22.15", + "@overleaf/fetch-utils": "*", "aws-sdk": "^2.1174.0", "axios": "^0.21.2", "babel-loader": "^9.1.3",