apify/crawlee · error · Error
Remote browser endpoint() must return a URL string or an obj
Error message
Remote browser endpoint() must return a URL string or an object with a non-empty 'url'.
What it means
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.
Source
Thrown at packages/browser-pool/src/remote-browser-pool.ts:81
constructor(
endpoint: RemoteBrowserEndpoint,
onRelease: ((info: { endpoint: string; context?: Record<string, unknown> }) => unknown) | undefined,
log: CrawleeLogger,
) {
this.#endpoint = endpoint;
this.#onRelease = onRelease;
this.#log = log;
}
async resolve(options?: { proxyUrl?: string }): Promise<{ url: string; token: number }> {
const resolved = typeof this.#endpoint === 'function' ? await this.#endpoint(options) : this.#endpoint;
let result: ResolvedRemoteEndpoint;
if (typeof resolved === 'string') {
if (!resolved) throw new Error('Remote browser endpoint resolved to an empty string.');
result = { url: resolved };
} else if (!resolved?.url) {
throw new Error("Remote browser endpoint() must return a URL string or an object with a non-empty 'url'.");
} else {
result = resolved;
}
const token = this.#nextToken++;
this.#sessions.set(token, { url: result.url, context: result.context, released: false });
return { url: result.url, token };
}
async release(token: number): Promise<void> {
const session = this.#sessions.get(token);
// Release at most once per session — guards a close()/teardown race (the `released` flag is set
// synchronously before the awaited onRelease, so releaseAll() can't double-fire an in-flight release).
if (!session || session.released) return;
session.released = true;
try {
await this.#onRelease?.({ endpoint: session.url, context: session.context });View on GitHub (pinned to dbe57fb09c)
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.
Example fix
// before
endpoint: async () => ({ endpoint: await getConnection() })
// after
endpoint: async () => ({ url: await getConnection() }) // must use the 'url' key Defensive patterns
Strategy: type-guard
Validate before calling
const resolved = await endpointFn();
if (typeof resolved === 'object' && resolved !== null && !('url' in resolved && resolved.url)) {
throw new Error(`Endpoint object missing url: ${JSON.stringify(Object.keys(resolved))}`);
} Type guard
function isResolvedRemoteEndpoint(v: unknown): v is { url: string; context?: unknown } {
return typeof v === 'object' && v !== null && typeof (v as { url?: unknown }).url === 'string' && (v as { url: string }).url.length > 0;
} Try / catch
try {
await remotePool.init();
} catch (err) {
if (err instanceof Error && err.message.includes("non-empty 'url'")) {
// normalize endpoint result to { url } shape and re-init
} throw err;
} Prevention
- 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.
When it happens
Trigger: Endpoint function returning an object like {} or { context: ... } without a url, or { url: '' }, to RemoteBrowserPool's endpoint option.
Common situations: 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.
Related errors
- Remote browser endpoint resolved to an empty string.
- ${crawlerName}: ${names.map((name) => `\`${name}\``).join(',
- Provided browserPlugin is not one of the plugins used by Bro
- Stagehand instance not found for browser
- Could not resolve StagehandController for page — is the brow
AI-assisted analysis of apify/crawlee@dbe57fb09c (2026-08-30).
Data as JSON: /api/errors/f94e8a6a6c1e30c3.
Report an issue: GitHub.