{"record":{"id":"2634dedac6f6846e","repo":"phacility/phabricator","slug":"only-static-resources-may-be-served","errorCode":null,"errorMessage":"Only static resources may be served.","messagePattern":"Only static resources may be served\\.","errorType":"exception","errorClass":"Exception","httpStatus":null,"severity":"error","filePath":"src/applications/celerity/controller/CelerityResourceController.php","lineNumber":41,"sourceCode":"  }\n\n  abstract public function getCelerityResourceMap();\n\n  protected function serveResource(array $spec) {\n    $path = $spec['path'];\n    $hash = idx($spec, 'hash');\n\n    // Sanity checking to keep this from exposing anything sensitive, since it\n    // ultimately boils down to disk reads.\n    if (preg_match('@(//|\\.\\.)@', $path)) {\n      return new Aphront400Response();\n    }\n\n    $type = CelerityResourceTransformer::getResourceType($path);\n    $type_map = self::getSupportedResourceTypes();\n\n    if (empty($type_map[$type])) {\n      throw new Exception(pht('Only static resources may be served.'));\n    }\n\n    $dev_mode = PhabricatorEnv::getEnvConfig('phabricator.developer-mode');\n\n    $map = $this->getCelerityResourceMap();\n    $expect_hash = $map->getHashForName($path);\n\n    // Test if the URI hash is correct for our current resource map. If it\n    // is not, refuse to cache this resource. This avoids poisoning caches\n    // and CDNs if we're getting a request for a new resource to an old node\n    // shortly after a push.\n    $is_cacheable = ($hash === $expect_hash);\n    $is_locally_cacheable = $this->isLocallyCacheableResourceType($type);\n    if (AphrontRequest::getHTTPHeader('If-Modified-Since') && $is_cacheable) {\n      // Return a \"304 Not Modified\". We don't care about the value of this\n      // field since we never change what resource is served by a given URI.\n      return $this->makeResponseCacheable(new Aphront304Response());\n    }","sourceCodeStart":23,"sourceCodeEnd":59,"githubUrl":"https://github.com/phacility/phabricator/blob/5720a38cfe95b00ca4be5016dd0d2f3195f4fa04/src/applications/celerity/controller/CelerityResourceController.php#L23-L59","documentation":"CelerityResourceController serves files from disk based on a request path, so it locks down what it will read: after rejecting path traversal ('//' or '..'), it maps the file extension to a type via getResourceType() and throws unless that type is in getSupportedResourceTypes() (css, js, png, svg, gif, jpg, swf, woff, woff2, eot, ...). This keeps the controller from becoming an arbitrary-file reader.","triggerScenarios":"A /res/ request for a file whose extension is not in the supported map, e.g. '.map', '.txt', '.json', or any unknown suffix; Adding a new asset type to a fork (e.g. '.webmanifest') and requesting it without extending getSupportedResourceTypes(); Stale URLs requesting files that were replaced with a different extension","commonSituations":"Forks shipping new static file kinds (source maps, manifest files) through celerity; browsers auto-requesting '<x>.map' for JS served from /res/; renames like .js to .mjs.","solutions":["Serve the file with a supported extension or from a different controller that is designed for that content","If you maintain a fork and genuinely need a new type, extend getSupportedResourceTypes() with the extension => content-type entry","For source-map noise, generate maps without deploying them, or publish them under a static host rather than /res/"],"exampleFix":"// before (fork serves /res/<hash>/js/app.js.map -> exception)\n\n// after: register the type in the controller\npublic static function getSupportedResourceTypes() {\n  return array(\n    'css' => 'text/css; charset=utf-8',\n    'js'  => 'text/javascript; charset=utf-8',\n    'map' => 'application/json; charset=utf-8',\n    // ...\n  );\n}","handlingStrategy":"validation","validationCode":"$ext = strtolower(last(explode('.', $path)));\n$supported = array_keys(CelerityResourceController::getSupportedResourceTypes());\nif (!in_array($ext, $supported, true)) {\n  // Serve the file through a different, purpose-built controller instead.\n  return new Aphront404Response();\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Only reference celerity resources with extensions present in getSupportedResourceTypes()","Do not point /res/ at generated artifacts like .map or .json — serve those elsewhere","If maintaining a fork that adds a type, extend the type map and add a test covering the new extension"],"tags":["phabricator","celerity","asset-pipeline","http-controller","security-guard"],"backgroundTag":"unsupported-content-type","analyzedSha":"5720a38cfe95b00ca4be5016dd0d2f3195f4fa04","analyzedAt":"2026-08-21T05:07:25.672Z","schemaVersion":2},"datasetVersion":"2026-08-21T11:28:35.574Z"}