Add retry logic and timeout to media proxy

This commit is contained in:
Zed 2026-06-19 22:00:28 +02:00
commit 581c6f1714

View file

@ -1,6 +1,7 @@
# SPDX-License-Identifier: AGPL-3.0-only # SPDX-License-Identifier: AGPL-3.0-only
import uri, strutils, httpclient, os, hashes, base64, re import uri, strutils, httpclient, os, hashes, base64, re
import asynchttpserver, asyncstreams, asyncfile, asyncnet import asynchttpserver, asyncstreams, asyncfile, asyncnet
import asyncdispatch
import jester import jester
@ -32,47 +33,70 @@ template respond*(req: asynchttpserver.Request; headers) =
proc proxyMedia*(req: jester.Request; url: string): Future[HttpCode] {.async.} = proc proxyMedia*(req: jester.Request; url: string): Future[HttpCode] {.async.} =
result = Http200 result = Http200
let let request = req.getNativeReq()
request = req.getNativeReq()
client = newAsyncHttpClient(maxRedirects = 0)
try: for attempt in 0 .. 2:
let res = await client.get(url) let client = newAsyncHttpClient(maxRedirects = 0)
if res.status != "200 OK": var shouldRetry = false
if res.status != "404 Not Found": try:
echo "[media] Proxying failed, status: $1, url: $2" % [res.status, url] let resFut = client.get(url)
return Http404 let completed = await withTimeout(resFut, 5000)
if not completed:
let hashed = $hash(url) if attempt < 2:
if request.headers.getOrDefault("If-None-Match") == hashed: echo "[media] Retry $1/2, timeout after 5s, url: $2" % [$(attempt + 1), url]
return Http304 shouldRetry = true
else:
let contentLength = echo "[media] Proxying timeout after 5s, url: $1" % [url]
if res.headers.hasKey("content-length"): return Http504
res.headers["content-length", 0]
else: else:
"" let res = resFut.read()
if res.status != "200 OK":
if res.status == "404 Not Found":
return Http404
if attempt < 2:
echo "[media] Retry $1/2, status: $2, url: $3" % [$(attempt + 1), res.status, url]
shouldRetry = true
else:
echo "[media] Proxying failed, status: $1, url: $2" % [res.status, url]
return Http404
else:
let hashed = $hash(url)
if request.headers.getOrDefault("If-None-Match") == hashed:
return Http304
let headers = newHttpHeaders({ let contentLength =
"content-type": res.headers["content-type", 0], if res.headers.hasKey("content-length"):
"content-length": contentLength, res.headers["content-length", 0]
"cache-control": maxAge, else:
"etag": hashed ""
})
respond(request, headers) let headers = newHttpHeaders({
"content-type": res.headers["content-type", 0],
"content-length": contentLength,
"cache-control": maxAge,
"etag": hashed
})
var (hasValue, data) = (true, "") respond(request, headers)
while hasValue:
(hasValue, data) = await res.bodyStream.read() var (hasValue, data) = (true, "")
if hasValue: while hasValue:
await request.client.send(data) (hasValue, data) = await res.bodyStream.read()
data.setLen 0 if hasValue:
except HttpRequestError, ProtocolError, OSError: await request.client.send(data)
echo "[media] Proxying exception, error: $1, url: $2" % [getCurrentExceptionMsg(), url] data.setLen 0
result = Http404 return Http200
finally: except CatchableError:
client.close() if attempt < 2:
echo "[media] Retry $1/2, error: $2, url: $3" % [$(attempt + 1), getCurrentExceptionMsg(), url]
shouldRetry = true
else:
echo "[media] Proxying exception, error: $1, url: $2" % [getCurrentExceptionMsg(), url]
result = Http404
finally:
client.close()
if not shouldRetry:
break
template check*(code): untyped = template check*(code): untyped =
if code != Http200: if code != Http200:
@ -129,7 +153,7 @@ proc createMediaRouter*(cfg: Config) =
if getHmac(url) != request.matches[1]: if getHmac(url) != request.matches[1]:
resp Http403, showError("Failed to verify signature", cfg) resp Http403, showError("Failed to verify signature", cfg)
if ".mp4" in url or ".ts" in url or ".m4s" in url: if ".mp4" in url or ".ts" in url or ".m4s" in url or ".aac" in url:
let code = await proxyMedia(request, url) let code = await proxyMedia(request, url)
check code check code