{"record":{"id":"c6b03ee566a8709d","repo":"immich-app/immich","slug":"urlerror-badurl","errorCode":null,"errorMessage":"URLError.badURL","messagePattern":"URLError\\.badURL","errorType":"exception","errorClass":"URLError","httpStatus":null,"severity":"error","filePath":"mobile/ios/WidgetExtension/ImmichAPI.swift","lineNumber":189,"sourceCode":"  \n  func applyCustomHeaders(for request: inout URLRequest) {\n    for (header, value) in serverConfig.customHeaders {\n      request.addValue(value, forHTTPHeaderField: header)\n    }\n  }\n\n  func fetchSearchResults(with filters: SearchFilter = Album.NONE.filter)\n    async throws\n    -> [Asset]\n  {\n    // get URL\n    guard\n      let searchURL = buildRequestURL(\n        serverConfig: serverConfig,\n        endpoint: \"/search/random\"\n      )\n    else {\n      throw URLError(.badURL)\n    }\n\n    var request = URLRequest(url: searchURL)\n    request.httpMethod = \"POST\"\n    request.httpBody = try JSONEncoder().encode(filters)\n    request.setValue(\"application/json\", forHTTPHeaderField: \"Content-Type\")\n    applyCustomHeaders(for: &request)\n    \n    let (data, _) = try await URLSession.shared.data(for: request)\n\n    // decode data\n    return try JSONDecoder().decode([Asset].self, from: data)\n  }\n\n  func fetchMemory(for date: Date) async throws -> [MemoryResult] {\n    // get URL\n    let localDay = date.formatted(\n      Date.ISO8601FormatStyle(timeZone: .current).year().month().day().dateSeparator(.dash)","sourceCodeStart":171,"sourceCodeEnd":207,"githubUrl":"https://github.com/immich-app/immich/blob/e55ac299a4ec7cb372e35dbf2c6c05ee9ce77f6c/mobile/ios/WidgetExtension/ImmichAPI.swift#L171-L207","documentation":"The widget's random-search call builds its request URL from the shared server configuration; when buildRequestURL cannot construct a valid URL for the /search/random endpoint it throws URLError(.badURL).","triggerScenarios":"randomSearch invoked while serverConfig lacks a usable serverUrl (nil/empty/invalid), making buildRequestURL return nil for the /search/random endpoint.","commonSituations":"Widget credentials/server config never synced into the app group; user logged out in the main app leaving stale/blank config; server URL entered without scheme.","solutions":["Ensure the main app writes a valid, normalized serverUrl into the shared app-group config before widget use","Validate URL formation (scheme + host) at save time and on widget launch","Fall back to a configuration-required widget state when serverConfig is invalid"],"exampleFix":"// before\nthrow URLError(.badURL) // silent misconfig\n// after\nguard let serverUrl = serverConfig.serverUrl, URL(string: serverUrl)?.host != nil else {\n  return .failure(.needsConfiguration) // explicit widget state\n}","handlingStrategy":"validation","validationCode":"let canFetch: Bool {\n  guard let raw = serverConfig.serverUrl,\n        let url = URL(string: raw),\n        let scheme = url.scheme,\n        [\"http\", \"https\"].contains(scheme),\n        url.host != nil else { return false }\n  return true\n}","typeGuard":"func isValidServerURL(_ raw: String?) -> Bool {\n  guard let raw, let url = URL(string: raw), url.host != nil else { return false }\n  return [\"http\", \"https\"].contains(url.scheme ?? \"\")\n}","tryCatchPattern":"do {\n  let results = try await api.randomSearch(...)\n} catch URLError.badURL {\n  widgetState = .needsConfiguration\n}","preventionTips":["Validate server URL (scheme + host) when saving config","Keep widget config in the shared app group in sync with the app","Show a setup state instead of fetching with invalid config"],"tags":["ios","widget","url","configuration"],"backgroundTag":"invalid-url","analyzedSha":"e55ac299a4ec7cb372e35dbf2c6c05ee9ce77f6c","analyzedAt":"2026-09-15T07:20:19.675Z","contentChangedAt":"2026-09-15T07:20:19.675Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}