Timeout with fetch


#1

Is there currently a way to “fail-proof” fetch requests?

We have a license check API written in Workers that fetches our backend. In case the backend is not responding correctly, we will still allow the license to be valid (to minimize downtime).

This works with HTTP 500 responses, but not with timeouts as Workers abort after 15 seconds. Is there a way to try for about 12 seconds and then still respond with valid?

This is my current code (which is commented out at the moment):

try {
      var lccheck = await fetch(`URL`, {
              method: 'GET',
              headers: {'License-Key': lk}
          })
          if (lccheck.ok)
          {
            var lccheckresp = await lccheck.json()
            if(!lccheckresp.valid) output.valid = 0
          }
    } catch(err) {
      output.valid = 1
    }

#2

Create a timer that count every second, then after 12 second, abort the fetch using:

And then return the fail response.

The Abort method works in Chrome 66, I’m not sure if it works in Cloudflares customized engine.


#3

Sadly, no :frowning:

Uncaught (in promise) ReferenceError: AbortController is not defined
at handleRequest (worker.js:12:20)

#4

@KentonVarda Can we get this implemented/updated?

It’s quite important to be able to abort a request, especially if it’s towards a queue-based API where if it takes too long it really shouldn’t enter the system because it’s already been re-queued by another request.


#5

Hi @thomas4,

Even if we implemented the AbortController API, I’m not sure it would accomplish what you’re expecting here. The HTTP protocol does not include any way to “un-send” a request or to request that the server stop processing it. The AbortController API merely provides a way to instruct the browser that it should close the connection associated with the HTTP request and free up any client-side resources. A very smart server might recognize the connection closure and terminate execution, but most servers do not do this, and there is generally no guarantee that the connection closure ever reaches the server, especially when proxies are involved.

In the context of Cloudflare Workers, we don’t currently provide the AbortController API, but we do the same cleanup automatically when the FetchEvent handler completes. Any outstanding fetch() calls are automatically canceled once your worker has finished sending a response. This includes closing connections in cases where the response body hasn’t been received.

So, for now, the best way to “cancel” a fetch() is to stop paying attention after some period of time. You can use Promise.race() to accomplish this. Here’s an example:

// Initiate the fetch but don't await it yet, just keep the promise.
let fetchPromise = fetch(request)

// Create a promise that resolves to `undefined` after 10 seconds.
let timeoutPromise = new Promise(resolve => setTimeout(resolve, 10000))

// Wait for whichever promise completes first.
let response = await Promise.race([fetchPromise, timeoutPromise])

if (response) {
  // success, use response
} else {
  // timed out, handle error
}

#6

Hi @cat24max,

Workers do not “abort” after 15 seconds, but they do prohibit new outbound requests after this time, which seems to be the problem you are facing as you’d like to start a new request once the license check passes.

Here’s an idea: You could do both requests in parallel, but wait for the license check to complete before you start sending the content. This way, all requests are started immediately, so the 15-second time limit for starting new requests doesn’t apply. Here’s what that could look like:

// Start sending license check.
let lccheckPromise = fetch(licenseCheckUrl, headers: {'License-Key': lk})

// Start requesting the actual content, but don't send it to the client until
// the license check passes.
let contentPromise = fetch(request)

// Wait for the license check to finish.
let lccheckResponse = await lccheckPromise

if (!response.ok) {
  // The license check server returned a 4xx or 5xx status code.
  // We "fail open", sending the content even though we're not sure if
  // the client has a license.
  return contentPromise
}

// Parse the licence check response.
let lccheckJson = await lccheckResponse.json()

if (lccheckJson.valid) {
  // License check passed. Send content to client.
  return contentPromise
} else {
  // License not valid. Send error to client.
  return new Response("Unauthorized", {status: 403})
}

You could additionally extend this with a timeout (as in my previous comment) if you don’t want to make the user wait forever for the license check – but this is up to you and not necessary to stay under any limits imposed by Workers.