Crosstalk-Solutions/project-nomad · error

Could not fetch directory listing from the provided URL

Error message

Could not fetch directory listing from the provided URL

What it means

Catch-all 502 from browseLibrary when fetching the directory listing from the provided library URL fails for a non-SSRF reason: host unreachable, connection refused, timeout, TLS error, or the response is not a parseable directory index. The proxy-style 502 signals the failure belongs to the upstream URL, not this server.

Source

Thrown at admin/app/controllers/zim_controller.ts:224

      await this.zimService.removeCustomLibrary(payload.params.id)
      return { message: 'Custom library removed' }
    } catch (error) {
      if (error.message === 'Custom library not found') {
        return response.status(404).send({ message: error.message })
      }
      throw error
    }
  }

  async browseLibrary({ request, response }: HttpContext) {
    const payload = await request.validateUsing(browseLibraryValidator)
    try {
      return await this.zimService.browseLibraryUrl(payload.url)
    } catch (error) {
      if (error.message?.includes('loopback or link-local')) {
        return response.status(400).send({ message: error.message })
      }
      return response.status(502).send({
        message: 'Could not fetch directory listing from the provided URL',
      })
    }
  }
}

View on GitHub (pinned to 0bd1c6f4f9)

Solutions

  1. curl the URL from the admin server host to confirm it is reachable and returns a plain directory listing (links ending in .zim)
  2. Fix scheme/port/hostname typos and ensure the server serves an autoindex-style listing the parser understands
  3. If TLS fails, install/point to a valid certificate or use http within a trusted network
  4. Increase the fetch timeout in zimService.browseLibraryUrl for slow mirrors

Example fix

// before
await zimController.browseLibrary({ url: 'https://mirror.example.org/zims/' }) // 502
// after
# verify from the same host first:
# curl -sSL https://mirror.example.org/zims/ | head
await zimController.browseLibrary({ url: 'https://mirror.example.org/en/zims/' }) // valid listing path
Defensive patterns

Strategy: fallback

Validate before calling

// pre-flight the URL from the same host/network before calling browse
const ok = await fetch(url, { method: 'HEAD', signal: AbortSignal.timeout(5000) })
if (!ok.ok) throw new Error(`Library URL unreachable (${ok.status})`)

Try / catch

try {
  return await zimService.browseLibraryUrl(url)
} catch (e) {
  if (e.message?.includes('loopback or link-local')) throw e // SSRF — do not retry
  // fallback: try a mirror or cached listing
  return cachedListing(url) ?? []
}

Prevention

When it happens

Trigger: Calling browse-library with a URL that is down/unreachable, serves a non-HTML/non-directory-index page, requires auth, or has a bad certificate; any fetch error not matching the loopback/link-local message.

Common situations: Library server stopped or firewall blocking the admin server's egress; URL points to an HTML page or a native OPDS feed instead of an Apache/nginx-style directory listing; self-signed cert causing TLS rejection; slow mirror timing out.

Related errors


AI-assisted analysis of Crosstalk-Solutions/project-nomad@0bd1c6f4f9 (2026-08-27). Data as JSON: /api/errors/edfe3b61396c5578. Report an issue: GitHub.