I am writing a basic web framework which can send files in chunks instead of copying them in full to memory.
When testing pages with many images, a lot of them end up corrupted even though individually downloading them with curl gives the correct data. I am suspecting that there is an interaction between the "Connection: keep-alive" browser behaviour that reuses connections for multiple files, and async writes the client socket. In particular, setting "Connection: close" removes the corruption problem.
Any idea why this occurs and how I could implement file sending while keeping connections alive?
Here is my implementation: https://gist.github.com/benob/63e6570685e16d6216ad7e41cb2a6c73
To reproduce the problem, put an html file which refers to many images (and images) in "public/"
You forget to return after await req.sendFile(filename). I also added content-type header. I seems working fine for 1000 images.
import asynchttpserver, asyncdispatch
import asyncnet
import os
import strutils
import strformat
import asyncfile
import uri
const chunk_size = 4 * 1024
proc sendFile*(req: Request, filename: string){.async.} =
var file = openAsync(filename)
let size = file.getFileSize()
var msg = "HTTP/1.1 200\c\L"
msg.add("Content-Length: ")
msg.addInt size
msg.add("Content-Type: application/octet-stream\c\L")
#msg.add "\c\L"
#msg.add "Connection: close\c\L"
msg.add "\c\L"
await req.client.send(msg)
var remaining = size
var buffer: array[chunk_size, byte]
while remaining > 0:
let read = await file.readBuffer(addr buffer[0], chunk_size)
await req.client.send(addr buffer[0], read)
remaining -= read
file.close()
type Route = object
path: string
cb: proc(req: Request){.async,gcsafe.}
type App* = object
server: AsyncHttpServer
routes: seq[Route]
staticRoutes: seq[string]
proc newApp*(): App =
result.server = newAsyncHttpServer()
proc get*(app: var App, path: string, cb: proc(req: Request){.async,gcsafe.}) =
let route = Route(path: path, cb: cb)
app.routes.add(route)
proc static*(app: var App, path: string) =
app.staticRoutes.add(path)
proc run*(app: App) {.async.} =
proc cb(req: Request) {.async,gcsafe.} =
for route in app.routes:
if route.path == req.url.path:
await route.cb(req)
return
for path in app.staticRoutes:
if req.url.path.startswith(path):
let filename = currentSourcePath().parentDir / decodeUrl(req.url.path)
echo filename
if fileExists(filename):
await req.sendFile(filename)
return
await req.respond(Http404, "Not Found")
await app.server.serve(Port(3333), cb)
when isMainModule:
var app = newApp()
app.get("/", proc(req: Request) {.async,gcsafe.} =
# prepare with `for i in {1..1000}; do cp test.jpg test_$i.jpg; done`
var html = ""
for i in 1..1000:
html.add fmt"""<img src="/public/test_{i}.jpg" style="width: 100px"/>"""
await req.respond(Http200, html)
)
app.static("/public")
waitFor app.run()