Merge pull request #21011 from overleaf/ls-serve-static-wrapper

add a wrapper for serve static to handle premature close error

GitOrigin-RevId: 8128702d9340a893624061d07bf0da15ea457f43
This commit is contained in:
Liangjun Song 2024-10-14 15:57:13 +08:00 committed by Copybot
parent 3be3bf9037
commit 8c342dc226
3 changed files with 104 additions and 4 deletions

View file

@ -0,0 +1,34 @@
import express from 'express'
import { plainTextResponse } from './Response.js'
/*
This wrapper is implemented specifically to handle "Premature Close" errors.
These errors occur when the client cancels a request while static assets are being loaded.
This issue is beyond our control, it can result in unnecessary log noise.
Therefore, this wrapper is added to handle such errors.
*/
function serveStaticWrapper(root, options) {
const serveStatic = express.static(root, options)
return (req, res, next) => {
serveStatic(req, res, error => {
if (!error) {
return next()
}
if (error.code !== 'ERR_STREAM_PREMATURE_CLOSE') {
return next(error)
}
req.logger.addFields({ err: error })
req.logger.setLevel('debug')
if (res.headersSent) {
res.end()
} else {
res.status(400)
plainTextResponse(res, 'Premature close')
}
})
}
}
export default serveStaticWrapper

View file

@ -37,6 +37,7 @@ import noCache from 'nocache'
import os from 'os' import os from 'os'
import http from 'http' import http from 'http'
import { fileURLToPath } from 'url' import { fileURLToPath } from 'url'
import serveStaticWrapper from './ServeStaticWrapper.mjs'
const sessionsRedisClient = UserSessionsRedis.client() const sessionsRedisClient = UserSessionsRedis.client()
@ -120,10 +121,13 @@ if (Settings.exposeHostname) {
} }
webRouter.use( webRouter.use(
express.static(fileURLToPath(new URL('../../../public', import.meta.url)), { serveStaticWrapper(
maxAge: STATIC_CACHE_AGE, fileURLToPath(new URL('../../../public', import.meta.url)),
setHeaders: csp.removeCSPHeaders, {
}) maxAge: STATIC_CACHE_AGE,
setHeaders: csp.removeCSPHeaders,
}
)
) )
app.set('views', fileURLToPath(new URL('../../views', import.meta.url))) app.set('views', fileURLToPath(new URL('../../views', import.meta.url)))

View file

@ -0,0 +1,62 @@
import { strict as esmock } from 'esmock'
import { expect } from 'chai'
import Path from 'node:path'
import { fileURLToPath } from 'node:url'
import sinon from 'sinon'
import MockResponse from '../helpers/MockResponse.js'
import MockRequest from '../helpers/MockRequest.js'
const __dirname = fileURLToPath(new URL('.', import.meta.url))
const modulePath = Path.join(
__dirname,
'../../../../app/src/infrastructure/ServeStaticWrapper'
)
describe('ServeStaticWrapperTests', function () {
let error = null
beforeEach(async function () {
this.req = new MockRequest()
this.res = new MockResponse()
this.express = {
static: () => (req, res, next) => {
if (error) {
next(error)
} else {
next()
}
},
}
this.serveStaticWrapper = await esmock(modulePath, {
express: this.express,
})
})
this.afterEach(() => {
error = null
})
it('Premature close error thrown', async function () {
error = new Error()
error.code = 'ERR_STREAM_PREMATURE_CLOSE'
const middleware = this.serveStaticWrapper('test_folder', {})
const next = sinon.stub()
middleware(this.req, this.res, next)
expect(next.called).to.be.false
})
it('No error thrown', async function () {
const middleware = this.serveStaticWrapper('test_folder', {})
const next = sinon.stub()
middleware(this.req, this.res, next)
expect(next).to.be.calledWith()
})
it('Other error thrown', async function () {
error = new Error()
const middleware = this.serveStaticWrapper('test_folder', {})
const next = sinon.stub()
middleware(this.req, this.res, next)
expect(next).to.be.calledWith(error)
})
})