{"record":{"id":"722008a5a9bbd46b","repo":"Kareadita/Kavita","slug":"query-required","errorCode":null,"errorMessage":"query-required","messagePattern":"query-required","errorType":"exception","errorClass":"OpdsException","httpStatus":null,"severity":"error","filePath":"Kavita.Services/OpdsService.cs","lineNumber":729,"sourceCode":"            $\"{apiKey}/series/{seriesId}/volume/{volumeId}/chapter/{chapterId}\", apiKey, prefix);\n        SetFeedId(feed, $\"series-{series.Id}-volume-{volumeId}-{chapterId}-files\");\n\n\n        feed.Entries.Add(CreateChapterFeedEntry(series, volume, chapter, namingContext, request));\n\n        return feed;\n    }\n\n    public async Task<Feed> Search(OpdsSearchRequest request, CancellationToken ct = default)\n    {\n        var userId = UnpackRequest(request, out var apiKey, out var prefix, out var baseUrl);\n        var query = request.Query;\n\n        var user = await unitOfWork.UserRepository.GetUserByIdAsync(userId, ct: ct);\n\n        if (string.IsNullOrEmpty(query))\n        {\n            throw new  OpdsException(await localizationService.TranslateAsync(userId, \"query-required\"));\n        }\n        query = query.Replace(\"%\", string.Empty);\n\n        var libraries = (await unitOfWork.LibraryRepository.GetLibraryIdsForUserIdAsync(userId, QueryContext.Search, ct)).ToList();\n        if (libraries.Count == 0)\n        {\n            throw new OpdsException(await localizationService.TranslateAsync(userId, \"libraries-restricted\"));\n        }\n\n        var isAdmin = await unitOfWork.UserRepository.IsUserAdminAsync(user, ct);\n\n        // OPDS only strips '%', so ':' survives and shortcodes (anilist:/al:, mangabaka:/mb:, hardcover:) still parse\n        var searchDto = SearchDto.FromQuery(query, includeChapterAndFiles: false);\n        if (!searchDto.HasShortcode)\n        {\n            searchDto.Query = query;\n        }\n","sourceCodeStart":711,"sourceCodeEnd":747,"githubUrl":"https://github.com/Kareadita/Kavita/blob/9c3e5400007f8a0282f7d883f2ad5e71716e514d/Kavita.Services/OpdsService.cs#L711-L747","documentation":"Thrown by OpdsService.Search when the query string is null or empty. This is the first validation in the OPDS search method: the query parameter from OpdsSearchRequest.Query is checked with string.IsNullOrEmpty before any search logic runs. The error prevents executing an empty search against the database, which would return unbounded or meaningless results.","triggerScenarios":"An OPDS client sends a GET request to {apiKey}/series with a missing or empty query parameter. The OpdsSearchRequest is constructed with Query = null or Query = \"\". This can happen when the OPDS reader's search form is submitted without text, or when the search URL is constructed programmatically without a query value.","commonSituations":"An OPDS reader app's search form allows empty submission. A client bug constructs the search URL without including the query parameter. A user bookmarks or scripts a search URL with a blank query. The OPDS search template (OpenSearch description) is misconfigured on the client side.","solutions":["Ensure the OPDS client always sends a non-empty query string when invoking the search endpoint.","Add client-side validation to prevent empty search submissions.","If the OPDS reader uses an OpenSearch template, verify the template includes the {searchTerms} parameter correctly.","Check the URL construction logic in the OPDS client to ensure the query parameter is always populated.","Handle the 400 response gracefully by prompting the user to enter search terms."],"exampleFix":"// In the OPDS client, validate before requesting:\n// before (may send empty query):\n//   var url = $\"{baseUrl}/{apiKey}/series?query={searchBox.Text}\";\n//   var feed = await opdsClient.GetFeedAsync(url);\n// after:\n//   var query = searchBox.Text?.Trim();\n//   if (string.IsNullOrEmpty(query))\n//   {\n//       ShowMessage(\"Please enter a search term.\");\n//       return;\n//   }\n//   var url = $\"{baseUrl}/{apiKey}/series?query={Uri.EscapeDataString(query)}\";\n//   var feed = await opdsClient.GetFeedAsync(url);","handlingStrategy":"validation","validationCode":"// Before calling the OPDS search endpoint, validate the query:\n// var query = request.Query?.Trim();\n// if (string.IsNullOrEmpty(query))\n// {\n//     return BadRequest(\"Search query is required.\");\n// }\n// var searchRequest = request with { Query = query };\n// var feed = await opdsService.Search(searchRequest, ct);","typeGuard":null,"tryCatchPattern":"// try { var feed = await opdsService.Search(request, ct); }\n// catch (OpdsException ex) when (ex.Message.Contains(\"query-required\"))\n// {\n//     // Client submitted an empty search.\n//     return BadRequest(\"Please enter a search term.\");\n// }","preventionTips":["Add client-side validation to prevent empty search submissions in OPDS readers.","Verify the OpenSearch template in the OPDS client includes the {searchTerms} parameter correctly.","Check URL construction to ensure the query parameter is always populated.","Handle 'query-required' responses by prompting the user to enter search terms.","Trim whitespace from search input before sending to avoid edge cases."],"tags":["opds","search","validation","empty-input","feed"],"backgroundTag":null,"analyzedSha":"9c3e5400007f8a0282f7d883f2ad5e71716e514d","analyzedAt":"2026-08-13T19:06:05.897Z","schemaVersion":2},"datasetVersion":"2026-08-13T19:17:28.613Z"}