{"record":{"id":"cbf30648fc30e049","repo":"RocketChat/Rocket.Chat","slug":"the-environmental-variable-envvarname-is-not","errorCode":null,"errorMessage":"The environmental variable \"${envVarName}\" is not readable.","messagePattern":"The environmental variable \"(.+?)\" is not readable\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"apps/meteor/app/apps/server/bridges/environmental.ts","lineNumber":16,"sourceCode":"import type { IAppServerOrchestrator } from '@rocket.chat/apps';\nimport { EnvironmentalVariableBridge } from '@rocket.chat/apps/dist/server/bridges/EnvironmentalVariableBridge';\n\nexport class AppEnvironmentalVariableBridge extends EnvironmentalVariableBridge {\n\tallowed: Array<string>;\n\n\tconstructor(private readonly orch: IAppServerOrchestrator) {\n\t\tsuper();\n\t\tthis.allowed = ['NODE_ENV', 'ROOT_URL', 'INSTANCE_IP'];\n\t}\n\n\tprotected async getValueByName(envVarName: string, appId: string): Promise<string | undefined> {\n\t\tthis.orch.debugLog(`The App ${appId} is getting the environmental variable value ${envVarName}.`);\n\n\t\tif (!(await this.isReadable(envVarName, appId))) {\n\t\t\tthrow new Error(`The environmental variable \"${envVarName}\" is not readable.`);\n\t\t}\n\n\t\treturn process.env[envVarName];\n\t}\n\n\tprotected async isReadable(envVarName: string, appId: string): Promise<boolean> {\n\t\tthis.orch.debugLog(`The App ${appId} is checking if the environmental variable is readable ${envVarName}.`);\n\n\t\treturn this.allowed.includes(envVarName.toUpperCase()) || this.isAppsOwnVariable(envVarName, appId);\n\t}\n\n\tprotected isAppsOwnVariable(envVarName: string, appId: string): boolean {\n\t\t/**\n\t\t * Replace the letter `-` with `_` since environment variable name doesn't support it\n\t\t */\n\t\tconst appVariablePrefix = `RC_APPS_${appId.toUpperCase().replace(/-/g, '_')}`;\n\t\treturn envVarName.toUpperCase().startsWith(appVariablePrefix);\n\t}","sourceCodeStart":1,"sourceCodeEnd":34,"githubUrl":"https://github.com/RocketChat/Rocket.Chat/blob/b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0/apps/meteor/app/apps/server/bridges/environmental.ts#L1-L34","documentation":"Catch-all 404 for FileSystem:Uploads.get: fsp.stat threw (usually ENOENT — the Uploads document exists in Mongo but the file is gone from disk), or a later step (Range parsing via getFileRange, stream creation) threw. The original error is swallowed; only a bodyless 404 reaches the client, which makes root-causing harder.","triggerScenarios":"Upload record exists but the file was deleted from the disk store: FileUpload_FileSystemPath changed or the volume is not mounted; disk cleanup jobs pruned uploads; DB restored without the files directory. Also fires when getFileRange throws on a malformed Range header or the read stream cannot be created (EACCES).","commonSituations":"Docker/K8s volume mounts missing after restart; migrations between storage types; DB-only backups; aggressive disk cleanup; permission changes on the uploads directory.","solutions":["Confirm FileUpload_FileSystemPath still points at the volume that actually holds the uploads","Restore the missing files from backup, or clean up the dangling Uploads records if the loss is accepted","Add temporary logging in the catch to surface the real error (ENOENT vs EACCES vs range parsing) before deciding","Check the Range header of failing clients if only some requests 404"],"exampleFix":"// before\n} catch (e) {\n\tres.writeHead(404);\n\tres.end();\n}\n// after\n} catch (e) {\n\tSystemLogger.error({ msg: 'file serve failed', fileId: file._id, code: e.code, message: e.message });\n\tres.writeHead(404);\n\tres.end();\n}","handlingStrategy":"try-catch","validationCode":"const ok = await fsp.stat(await store.getFilePath(file._id, file)).then((s) => s.isFile()).catch(() => false);\nif (!ok) { /* record exists but bytes missing: quarantine/report instead of 404 */ }","typeGuard":null,"tryCatchPattern":"Wrap the whole serve step in try/catch; inspect (e as NodeJS.ErrnoException).code — ENOENT means missing file (restore/re-upload), EACCES means permissions (fix ownership), anything else points at range/stream logic; log e with the fileId before responding 404.","preventionTips":["Log the underlying error in every catch that converts to 404 — a silent 404 hides ENOENT/EACCES/range bugs","Back up the filesystem uploads directory with the database","Alert on 404 spikes for /file-upload/* after deployments or volume changes"],"tags":["file-upload","filesystem","http-404","enoent","storage"],"backgroundTag":"file-missing-from-storage","analyzedSha":"b2c16d5842cbe6b69b59bdf6fc5e5f1afcd1f0b0","analyzedAt":"2026-08-18T15:26:39.429Z","contentChangedAt":"2026-08-18T15:26:39.429Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}