phacility/phabricator · error · Exception

Only static resources may be served.

Error message

Only static resources may be served.

What it means

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.

Source

Thrown at src/applications/celerity/controller/CelerityResourceController.php:41

  }

  abstract public function getCelerityResourceMap();

  protected function serveResource(array $spec) {
    $path = $spec['path'];
    $hash = idx($spec, 'hash');

    // Sanity checking to keep this from exposing anything sensitive, since it
    // ultimately boils down to disk reads.
    if (preg_match('@(//|\.\.)@', $path)) {
      return new Aphront400Response();
    }

    $type = CelerityResourceTransformer::getResourceType($path);
    $type_map = self::getSupportedResourceTypes();

    if (empty($type_map[$type])) {
      throw new Exception(pht('Only static resources may be served.'));
    }

    $dev_mode = PhabricatorEnv::getEnvConfig('phabricator.developer-mode');

    $map = $this->getCelerityResourceMap();
    $expect_hash = $map->getHashForName($path);

    // Test if the URI hash is correct for our current resource map. If it
    // is not, refuse to cache this resource. This avoids poisoning caches
    // and CDNs if we're getting a request for a new resource to an old node
    // shortly after a push.
    $is_cacheable = ($hash === $expect_hash);
    $is_locally_cacheable = $this->isLocallyCacheableResourceType($type);
    if (AphrontRequest::getHTTPHeader('If-Modified-Since') && $is_cacheable) {
      // Return a "304 Not Modified". We don't care about the value of this
      // field since we never change what resource is served by a given URI.
      return $this->makeResponseCacheable(new Aphront304Response());
    }

View on GitHub (pinned to 5720a38cfe)

Solutions

  1. Serve the file with a supported extension or from a different controller that is designed for that content
  2. If you maintain a fork and genuinely need a new type, extend getSupportedResourceTypes() with the extension => content-type entry
  3. For source-map noise, generate maps without deploying them, or publish them under a static host rather than /res/

Example fix

// before (fork serves /res/<hash>/js/app.js.map -> exception)

// after: register the type in the controller
public static function getSupportedResourceTypes() {
  return array(
    'css' => 'text/css; charset=utf-8',
    'js'  => 'text/javascript; charset=utf-8',
    'map' => 'application/json; charset=utf-8',
    // ...
  );
}
Defensive patterns

Strategy: validation

Validate before calling

$ext = strtolower(last(explode('.', $path)));
$supported = array_keys(CelerityResourceController::getSupportedResourceTypes());
if (!in_array($ext, $supported, true)) {
  // Serve the file through a different, purpose-built controller instead.
  return new Aphront404Response();
}

Prevention

When it happens

Trigger: 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

Common situations: 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.

Related errors


AI-assisted analysis of phacility/phabricator@5720a38cfe (2026-08-21). Data as JSON: /api/errors/2634dedac6f6846e. Report an issue: GitHub.