{"record":{"id":"fe8377ea7a01f1ae","repo":"koodo-reader/koodo-reader","slug":"not-found","errorCode":null,"errorMessage":"Not Found","messagePattern":"Not Found","errorType":"http","errorClass":null,"httpStatus":404,"severity":"error","filePath":"httpserver/opds.go","lineNumber":604,"sourceCode":"\t\thandleOPDSCover(w, r, key)\n\n\t// Book file download: /opds/download/{key}.{format}\n\tcase strings.HasPrefix(path, \"/opds/download/\"):\n\t\tfilename := strings.TrimPrefix(path, \"/opds/download/\")\n\t\tfilename = strings.Trim(filename, \"/\")\n\t\t// Strip extension to get key\n\t\tkey := filename\n\t\tif idx := strings.LastIndex(filename, \".\"); idx > 0 {\n\t\t\tkey = filename[:idx]\n\t\t}\n\t\tif key == \"\" {\n\t\t\thttp.Error(w, \"Missing book key\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\thandleOPDSDownload(w, r, key)\n\n\tdefault:\n\t\thttp.Error(w, \"Not Found\", http.StatusNotFound)\n\t}\n}\n","sourceCodeStart":586,"sourceCodeEnd":607,"githubUrl":"https://github.com/koodo-reader/koodo-reader/blob/7d40df41e05cfc0a341fe9ea66a12819b522b7fa/httpserver/opds.go#L586-L607","documentation":"The OPDS router in opdsHandler fell through all known route cases and hit the default branch, which answers 404 Not Found. This means the request path under /opds did not exactly match the root/books/search routes nor start with the book/, cover/, or download/ prefixes.","triggerScenarios":"Any authenticated GET to an unmatched /opds path, e.g. /opds/book (no trailing slash/segment... actually matches prefix — rather: /opds/unknown, /opds/Book/xyz (case-sensitive), /opds/books/, /opds/search?q=... with a different verb path like /opds/feed/123, or a typo such as /opds/boo/abc.","commonSituations":"Typo'd endpoints in OPDS client config (e.g. /opds/bookx/), case-sensitive path mismatches, older clients using a removed/renamed route, proxies or base-path rewrites that alter the URL so prefixes no longer match (e.g. serving under /reader/opds).","solutions":["Use one of the supported routes: /opds, /opds/books, /opds/search, /opds/search.xml, /opds/book/{key}, /opds/cover/{key}, /opds/download/{key}.{format}.","Start from the root catalog /opds and follow the hrefs it emits instead of hand-constructing URLs.","Check for typos and case sensitivity in the path; Go matching here is exact and case-sensitive.","If the server is behind a reverse proxy or sub-path, ensure the proxy strips the prefix so requests reach the router as /opds/... ."],"exampleFix":"// before\nGET /opds/book/abc/extra  -> if unmatched variant like GET /opds/Books\n// after\nGET /opds/books            # exact, lowercase, no trailing slash","handlingStrategy":"validation","validationCode":"const OPDS_ROUTES = /^\\/opds(\\/books|\\/search(\\.xml)?|\\/book\\/[^/]+|\\/cover\\/[^/]+|\\/download\\/[^/]+\\.[^/]+)?\\/?$/;\nfunction isKnownOpdsPath(p) { return OPDS_ROUTES.test(p); }","typeGuard":"function isOpdsPath(v) {\n  return typeof v === 'string' && v.startsWith('/opds') && isKnownOpdsPath(v);\n}","tryCatchPattern":"const res = await fetch(path);\nif (res.status === 404) {\n  throw new Error(`Unknown OPDS endpoint: ${path}. Valid: /opds, /opds/books, /opds/search, /opds/search.xml, /opds/book/{key}, /opds/cover/{key}, /opds/download/{key}.{format}`);\n}\nif (!res.ok) throw new Error(`OPDS request failed: ${res.status}`);","preventionTips":["Discover endpoints by parsing hrefs from the root /opds catalog instead of hardcoding paths.","Keep paths lowercase and exact — matching is case-sensitive and trailing-segment sensitive.","If reverse-proxied under a sub-path, configure the proxy to rewrite to /opds/... before the server."],"tags":["http","opds","not-found","routing"],"backgroundTag":"http-404-route-not-found","analyzedSha":"7d40df41e05cfc0a341fe9ea66a12819b522b7fa","analyzedAt":"2026-08-29T01:41:43.691Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}