{"record":{"id":"f94e8a6a6c1e30c3","repo":"apify/crawlee","slug":"remote-browser-endpoint-must-return-a-url-string","errorCode":null,"errorMessage":"Remote browser endpoint() must return a URL string or an object with a non-empty 'url'.","messagePattern":"Remote browser endpoint\\(\\) must return a URL string or an object with a non-empty 'url'\\.","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/browser-pool/src/remote-browser-pool.ts","lineNumber":81,"sourceCode":"    constructor(\n        endpoint: RemoteBrowserEndpoint,\n        onRelease: ((info: { endpoint: string; context?: Record<string, unknown> }) => unknown) | undefined,\n        log: CrawleeLogger,\n    ) {\n        this.#endpoint = endpoint;\n        this.#onRelease = onRelease;\n        this.#log = log;\n    }\n\n    async resolve(options?: { proxyUrl?: string }): Promise<{ url: string; token: number }> {\n        const resolved = typeof this.#endpoint === 'function' ? await this.#endpoint(options) : this.#endpoint;\n\n        let result: ResolvedRemoteEndpoint;\n        if (typeof resolved === 'string') {\n            if (!resolved) throw new Error('Remote browser endpoint resolved to an empty string.');\n            result = { url: resolved };\n        } else if (!resolved?.url) {\n            throw new Error(\"Remote browser endpoint() must return a URL string or an object with a non-empty 'url'.\");\n        } else {\n            result = resolved;\n        }\n\n        const token = this.#nextToken++;\n        this.#sessions.set(token, { url: result.url, context: result.context, released: false });\n        return { url: result.url, token };\n    }\n\n    async release(token: number): Promise<void> {\n        const session = this.#sessions.get(token);\n        // Release at most once per session — guards a close()/teardown race (the `released` flag is set\n        // synchronously before the awaited onRelease, so releaseAll() can't double-fire an in-flight release).\n        if (!session || session.released) return;\n        session.released = true;\n\n        try {\n            await this.#onRelease?.({ endpoint: session.url, context: session.context });","sourceCodeStart":63,"sourceCodeEnd":99,"githubUrl":"https://github.com/apify/crawlee/blob/dbe57fb09ca607ad59dcf998f3925ef9ac3bb26c/packages/browser-pool/src/remote-browser-pool.ts#L63-L99","documentation":"If the endpoint function returns a non-string (an object), RemoteBrowserPool requires it to contain a non-empty url property (optionally with a context). An object without url — or with url: '' — is invalid and throws this message. This validates the { url, context? } shape the pool expects.","triggerScenarios":"Endpoint function returning an object like {} or { context: ... } without a url, or { url: '' }, to RemoteBrowserPool's endpoint option.","commonSituations":"Typo in the returned field name (e.g. endpoint: or wsUrl: instead of url:); API responses mapped incorrectly so url lands in another key; null/undefined url after optional chaining from a provisioning service.","solutions":["Return the object as { url: '<ws-or-http-url>', context?: ... } with a non-empty url string.","Fix field-name mismatches in the mapping from your provisioning API to the { url, context } shape.","Coerce/fall back: if the resolved value has no url, either throw a clearer error or fall back to a string endpoint."],"exampleFix":"// before\nendpoint: async () => ({ endpoint: await getConnection() })\n// after\nendpoint: async () => ({ url: await getConnection() }) // must use the 'url' key","handlingStrategy":"type-guard","validationCode":"const resolved = await endpointFn();\nif (typeof resolved === 'object' && resolved !== null && !('url' in resolved && resolved.url)) {\n  throw new Error(`Endpoint object missing url: ${JSON.stringify(Object.keys(resolved))}`);\n}","typeGuard":"function isResolvedRemoteEndpoint(v: unknown): v is { url: string; context?: unknown } {\n  return typeof v === 'object' && v !== null && typeof (v as { url?: unknown }).url === 'string' && (v as { url: string }).url.length > 0;\n}","tryCatchPattern":"try {\n  await remotePool.init();\n} catch (err) {\n  if (err instanceof Error && err.message.includes(\"non-empty 'url'\")) {\n    // normalize endpoint result to { url } shape and re-init\n  } throw err;\n}","preventionTips":["Type the endpoint function's return as { url: string; context?: ... } so TS enforces the url key.","Map provisioning API fields explicitly (url: resp.wsEndpoint) instead of passing the response through.","Unit-test endpoint functions against the { url, context? } contract."],"tags":["browser-pool","remote-browser","configuration","shape-validation"],"backgroundTag":"invalid-endpoint-shape","analyzedSha":"dbe57fb09ca607ad59dcf998f3925ef9ac3bb26c","analyzedAt":"2026-08-30T22:22:28.328Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}