alexajuno, home

fetching user-supplied urls

recently i dug into an image download problem. a user gives us a url to their image, and we download it from there. most of the time it just works. but some of those requests, at some hours, go wrong in ways i didn’t expect, and i’m learning how to handle them.

to understand the problem, i had to relearn the network basics: the steps one request goes through, and what their error messages look like. a download is five steps, each one waiting on the one before:

step            what happens              takes about
1. dns          where is the host?        5 ms
2. tcp          open a line to it         10 ms
3. tls          agree on encryption       20 ms
4. first byte   ask, wait for the answer  80 ms
5. body         the file, in chunks       30 ms
                                          ------
                                          150 ms

the times are rough, from one real download.

each step can fail in two ways. loud: the other side says no, fast. silent: the other side says nothing, and only your own timer ends it. a loud failure costs almost nothing, a silent one costs whatever your timer is.

our downloads go through curl, so i learnt what each failure looks like from curl’s error messages, shortened here:

step           loud                     silent
1. dns         could not resolve host   resolving timed out
2. tcp         could not connect        connection timed out
3. tls         certificate has expired  connection timed out
4. first byte  a 403, 404 or 503        timed out, 0 bytes
5. body        n bytes missing          timed out, n of m bytes

one thing surprised me: when tcp or tls goes silent, the message is the same, so the error alone can’t tell you which of the two got stuck. the ones that hurt me were all silent: most while connecting, some before the first byte, a few mid-file.

so the first fix is the timer. one long timeout for the whole download is a guess, so i measured healthy downloads first, and they all finished each step much faster than the timeout. then i split it into three timers, each a bit above the slowest healthy one:

  • connect: tcp and tls.
  • silence, no byte for a while: the wait for the first byte, and any gap mid-file. a big file that keeps flowing is never cut, a dead one is.
  • whole: since silence alone never stops a server that sends a trickle.

the second fix is a retry. a host often runs on many servers, so when one is stuck, trying again might land on another one that works.

not every failure deserves a retry though:

  • a 4xx like 403 or 404: the answer won’t change. give up.
  • a timeout, a refused or cut connection: retry right away.
  • a 5xx or 429: the server is busy. wait a second first.

and only once. retries stack: if three layers each try three times, one request becomes 27 calls to a server that’s already struggling.

with both, a stuck server should cost a few seconds, and the retry should often get the file. this is just a design for now, it’s not on prod yet. once it is, i’ll look at the metrics to see how much it actually helps.

refs