about summary refs log tree commit diff stats
path: root/src/local
diff options
context:
space:
mode:
authorbptato <nincsnevem662@gmail.com>2024-03-11 19:15:24 +0100
committerbptato <nincsnevem662@gmail.com>2024-03-11 21:09:52 +0100
commitb789b0b076ef7aba3f5f6bbb4f6b7cadf596994c (patch)
tree388a265fd778cd8fc3e9fad9d734628aca0bd287 /src/local
parent8bf82ddbfb84b5ca32824a466380dae4df4ff31a (diff)
downloadchawan-b789b0b076ef7aba3f5f6bbb4f6b7cadf596994c.tar.gz
loader: rework process model
Originally we had several loader processes so that the loader did not
need asynchronity for loading several buffers at once. Since then, the
scope of what loader does has been reduced significantly, and with
that loader has become mostly asynchronous.

This patch finishes the above work as follows:

* We only fork a single loader process for the browser. It is a waste of
  resources to do otherwise, and would have made future work on a
  download manager very difficult.

* loader becomes (almost) fully async. Now the only sync part is a)
  processing commands and b) waiting for clients to consume responses.
  b) is a bit more problematic than a), but should not cause problems
  unless some other horrible bug exists in a client. (TODO: make it
  fully async.)
  This gives us a noticable improvement in CSS loading speed, since all
  resources can now be queried at once (even before the previous ones
  are connected).

* Buffers now only get processes when the *connection* is finished. So
  headers, status code, etc. are handled by the client, and the buffer
  is forked when the loader starts streaming the response body.
  As a result, mailcap entries can simply dup2 the first UNIX domain
  socket connection as their stdin. This allows us to remove the ugly
  (and slow) `canredir' hack, which required us to send file handles on
  a tour accross the entire codebase.

* The "cache" has been reworked somewhat:
	- Since canredir is gone, buffer-level requests usually start
	  in a suspended state, and are explicitly resumed only after
	  the client could decide whether it wants to cache the response.
	- Instead of a flag on Request and the URL as the cache key,
	  we now use a global counter and the special `cache:' scheme.

* misc fixes: referer_from is now actually respected by buffers (not
  just the pager), load info display should work slightly better, etc.
Diffstat (limited to 'src/local')
-rw-r--r--src/local/client.nim114
-rw-r--r--src/local/container.nim383
-rw-r--r--src/local/pager.nim830
3 files changed, 720 insertions, 607 deletions
diff --git a/src/local/client.nim b/src/local/client.nim
index f8e1433d..b707fe84 100644
--- a/src/local/client.nim
+++ b/src/local/client.nim
@@ -24,6 +24,7 @@ import html/event
 import io/bufstream
 import io/posixstream
 import io/promise
+import io/serialize
 import io/socketstream
 import js/base64
 import js/console
@@ -61,7 +62,6 @@ type
     consoleWrapper: ConsoleWrapper
     fdmap: Table[int, Container]
     feednext: bool
-    forkserver: ForkServer
     ibuf: string
     jsctx: JSContext
     jsrt: JSRuntime
@@ -78,6 +78,9 @@ type
 
 jsDestructor(Client)
 
+func forkserver(client: Client): ForkServer {.inline.} =
+  client.pager.forkserver
+
 func console(client: Client): Console {.jsfget.} =
   return client.consoleWrapper.console
 
@@ -454,31 +457,51 @@ proc consoleBuffer(client: Client): Container {.jsfget.} =
   return client.consoleWrapper.container
 
 proc acceptBuffers(client: Client) =
-  while client.pager.unreg.len > 0:
-    let (pid, stream) = client.pager.unreg.pop()
+  let pager = client.pager
+  while pager.unreg.len > 0:
+    let (pid, stream) = pager.unreg.pop()
     let fd = int(stream.fd)
     if fd in client.fdmap:
       client.selector.unregister(fd)
       client.fdmap.del(fd)
     else:
-      client.pager.procmap.del(pid)
+      pager.procmap.del(pid)
     stream.close()
-  var accepted: seq[Pid]
   let registerFun = proc(fd: int) =
     client.selector.unregister(fd)
     client.selector.registerHandle(fd, {Read, Write}, 0)
-  for pid, container in client.pager.procmap:
-    let stream = connectSocketStream(pid, buffered = false, blocking = true)
+  for item in pager.procmap:
+    let container = item.container
+    let stream = connectSocketStream(container.process, buffered = false)
     if stream == nil:
-      client.pager.alert("Error: failed to set up buffer")
+      pager.alert("Error: failed to set up buffer")
       continue
-    container.setStream(stream, registerFun)
+    let key = pager.addLoaderClient(container.process,
+      container.config.loaderConfig)
+    stream.swrite(key)
+    let loader = pager.loader
+    if item.fdin != -1:
+      let outputId = item.istreamOutputId
+      if container.cacheId == -1:
+        container.cacheId = loader.addCacheFile(outputId, loader.clientPid)
+      var outCacheId = container.cacheId
+      let pid = container.process
+      if item.fdout == item.fdin:
+        loader.shareCachedItem(container.cacheId, pid)
+        loader.resume(@[item.istreamOutputId])
+      else:
+        outCacheId = loader.addCacheFile(item.ostreamOutputId, pid)
+        loader.resume(@[item.istreamOutputId, item.ostreamOutputId])
+      # pass down fdout
+      container.setStream(stream, registerFun, item.fdout, outCacheId)
+    else:
+      # buffer is cloned, no need to cache anything
+      container.setCloneStream(stream, registerFun)
     let fd = int(stream.fd)
     client.fdmap[fd] = container
     client.selector.registerHandle(fd, {Read}, 0)
-    client.pager.handleEvents(container)
-    accepted.add(pid)
-  client.pager.procmap.clear()
+    pager.handleEvents(container)
+  pager.procmap.setLen(0)
 
 proc c_setvbuf(f: File, buf: pointer, mode: cint, size: csize_t): cint {.
   importc: "setvbuf", header: "<stdio.h>", tags: [].}
@@ -488,6 +511,19 @@ proc handleRead(client: Client, fd: int) =
     client.input().then(proc() =
       client.handlePagerEvents()
     )
+  elif (let i = client.pager.findConnectingBuffer(fd); i != -1):
+    client.selector.unregister(fd)
+    client.loader.unregistered.add(fd)
+    let (container, stream) = client.pager.connectingBuffers[i]
+    let response = stream.readResponse(container.request)
+    if response.body == nil:
+      client.pager.fail(container, response.getErrorMessage())
+    elif response.redirect != nil:
+      client.pager.redirect(container, response)
+      response.body.close()
+    else:
+      client.pager.connected(container, response)
+    client.pager.connectingBuffers.del(i)
   elif fd == client.forkserver.estream.fd:
     const BufferSize = 4096
     const prefix = "STDERR: "
@@ -560,11 +596,18 @@ proc handleError(client: Client, fd: int) =
     client.loader.onError(fd)
   elif fd in client.loader.unregistered:
     discard # already unregistered...
+  elif (let i = client.pager.findConnectingBuffer(fd); i != -1):
+    # bleh
+    let (container, stream) = client.pager.connectingBuffers[i]
+    client.pager.fail(container, "loader died while loading")
+    client.selector.unregister(fd)
+    stream.close()
+    client.pager.connectingBuffers.del(i)
   else:
     if fd in client.fdmap:
       let container = client.fdmap[fd]
       if container != client.consoleWrapper.container:
-        client.console.log("Error in buffer", $container.location)
+        client.console.log("Error in buffer", $container.url)
       else:
         client.consoleWrapper.container = nil
       client.selector.unregister(fd)
@@ -675,7 +718,7 @@ proc writeFile(client: Client, path: string, content: string) {.jsfunc.} =
 
 const ConsoleTitle = "Browser Console"
 
-proc addConsole(pager: Pager, interactive: bool, clearFun, showFun, hideFun:
+proc addConsole(pager: Pager; interactive: bool; clearFun, showFun, hideFun:
     proc()): ConsoleWrapper =
   if interactive:
     var pipefd: array[0..1, cint]
@@ -683,25 +726,14 @@ proc addConsole(pager: Pager, interactive: bool, clearFun, showFun, hideFun:
       raise newException(Defect, "Failed to open console pipe.")
     let url = newURL("stream:console").get
     let container = pager.readPipe0("text/plain", CHARSET_UNKNOWN, pipefd[0],
-      some(url), ConsoleTitle, canreinterpret = false)
-    pager.registerContainer(container)
+      url, ConsoleTitle, canreinterpret = false)
     let err = newPosixStream(pipefd[1])
     err.writeLine("Type (M-c) console.hide() to return to buffer mode.")
-    let console = newConsole(
-      err,
-      clearFun = clearFun,
-      showFun = showFun,
-      hideFun = hideFun
-    )
-    return ConsoleWrapper(
-      console: console,
-      container: container
-    )
+    let console = newConsole(err, clearFun, showFun, hideFun)
+    return ConsoleWrapper(console: console, container: container)
   else:
-    let err = newFileStream(stderr)
-    return ConsoleWrapper(
-      console: newConsole(err)
-    )
+    let err = newPosixStream(stderr.getFileHandle())
+    return ConsoleWrapper(console: newConsole(err))
 
 proc clearConsole(client: Client) =
   var pipefd: array[0..1, cint]
@@ -710,9 +742,8 @@ proc clearConsole(client: Client) =
   let url = newURL("stream:console").get
   let pager = client.pager
   let replacement = pager.readPipe0("text/plain", CHARSET_UNKNOWN, pipefd[0],
-    some(url), ConsoleTitle, canreinterpret = false)
+    url, ConsoleTitle, canreinterpret = false)
   replacement.replace = client.consoleWrapper.container
-  pager.registerContainer(replacement)
   client.consoleWrapper.container = replacement
   let console = client.consoleWrapper.console
   console.err.close()
@@ -726,7 +757,7 @@ proc dumpBuffers(client: Client) =
       client.pager.drawBuffer(container, ostream)
       client.pager.handleEvents(container)
     except IOError:
-      client.console.log("Error in buffer", $container.location)
+      client.console.log("Error in buffer", $container.url)
       # check for errors
       client.handleRead(client.forkserver.estream.fd)
       quit(1)
@@ -846,17 +877,16 @@ proc newClient*(config: Config, forkserver: ForkServer): Client =
   JS_SetModuleLoaderFunc(jsrt, normalizeModuleName, clientLoadJSModule, nil)
   let jsctx = jsrt.newJSContext()
   let pager = newPager(config, forkserver, jsctx)
+  let loader = forkserver.newFileLoader(LoaderConfig(
+    urimethodmap: config.getURIMethodMap(),
+    w3mCGICompat: config.external.w3m_cgi_compat,
+    cgiDir: pager.cgiDir,
+    tmpdir: pager.tmpdir
+  ))
+  pager.setLoader(loader)
   let client = Client(
     config: config,
-    forkserver: forkserver,
-    loader: forkserver.newFileLoader(
-      defaultHeaders = config.getDefaultHeaders(),
-      proxy = config.getProxy(),
-      urimethodmap = config.getURIMethodMap(),
-      cgiDir = pager.cgiDir,
-      acceptProxy = true,
-      w3mCGICompat = config.external.w3m_cgi_compat
-    ),
+    loader: loader,
     jsrt: jsrt,
     jsctx: jsctx,
     pager: pager
diff --git a/src/local/container.nim b/src/local/container.nim
index 88848ac7..d7ee5c64 100644
--- a/src/local/container.nim
+++ b/src/local/container.nim
@@ -7,22 +7,21 @@ when defined(posix):
 
 import config/config
 import display/term
-import extern/stdio
 import io/promise
 import io/serialize
 import io/socketstream
 import js/javascript
 import js/jstypes
 import js/regex
-import loader/connecterror
+import loader/headers
 import loader/loader
 import loader/request
 import local/select
 import server/buffer
-import server/forkserver
 import types/cell
 import types/color
 import types/cookie
+import types/referrer
 import types/url
 import utils/luwrap
 import utils/mimeguess
@@ -43,25 +42,24 @@ type
     setxsave: bool
 
   ContainerEventType* = enum
-    FAIL, SUCCESS, NEEDS_AUTH, REDIRECT, ANCHOR, NO_ANCHOR, UPDATE, READ_LINE,
-    READ_AREA, OPEN, INVALID_COMMAND, STATUS, ALERT, LOADED, TITLE,
-    CHECK_MAILCAP, QUIT
+    cetAnchor, cetNoAnchor, cetUpdate, cetReadLine, cetReadArea, cetOpen,
+    cetSetLoadInfo, cetStatus, cetAlert, cetLoaded, cetTitle
 
   ContainerEvent* = object
     case t*: ContainerEventType
-    of READ_LINE:
+    of cetReadLine:
       prompt*: string
       value*: string
       password*: bool
-    of READ_AREA:
+    of cetReadArea:
       tvalue*: string
-    of OPEN, REDIRECT:
+    of cetOpen:
       request*: Request
-    of ANCHOR, NO_ANCHOR:
+    of cetAnchor, cetNoAnchor:
       anchor*: string
-    of ALERT:
+    of cetAlert:
       msg*: string
-    of UPDATE:
+    of cetUpdate:
       force*: bool
     else: discard
 
@@ -94,9 +92,15 @@ type
   Container* = ref object
     # note: this is not the same as source.request.url (but should be synced
     # with buffer.url)
-    url: URL
-    #TODO this is inaccurate, because only the network charset is passed through
+    url* {.jsget.}: URL
+    #TODO this is inaccurate, because charsetStack can desync
     charset*: Charset
+    charsetStack*: seq[Charset]
+    # note: this is *not* the same as Buffer.cacheId. buffer has the cache ID of
+    # the output, while container holds that of the input. Thus pager can
+    # re-interpret the original input, and buffer can rewind the (potentially
+    # mailcap) output.
+    cacheId* {.jsget.}: int
     parent* {.jsget.}: Container
     children* {.jsget.}: seq[Container]
     config*: BufferConfig
@@ -113,8 +117,7 @@ type
     pos: CursorPosition
     bpos: seq[CursorPosition]
     highlights: seq[Highlight]
-    process* {.jsget.}: Pid
-    loaderPid* {.jsget.}: Pid
+    process* {.jsget.}: int
     loadinfo*: string
     lines: SimpleFlexibleGrid
     lineshift: int
@@ -146,22 +149,12 @@ type
 jsDestructor(Highlight)
 jsDestructor(Container)
 
-proc newBuffer*(forkserver: ForkServer, config: BufferConfig,
-    request: Request, attrs: WindowAttributes, title: string,
-    redirectdepth: int, canreinterpret: bool, fd: FileHandle,
-    contentType: Option[string]): Container =
-  let (process, loaderPid) = forkserver.forkBuffer(request, config, attrs)
-  if fd != -1:
-    loaderPid.passFd(request.url.host, fd)
-    if fd == 0:
-      # We are passing stdin.
-      closeStdin()
-    else:
-      discard close(fd)
+proc newContainer*(config: BufferConfig; url: URL; request: Request;
+    attrs: WindowAttributes; title: string; redirectdepth: int;
+    canreinterpret: bool; contentType: Option[string];
+    charsetStack: seq[Charset]; cacheId: int): Container =
   return Container(
-    url: request.url,
-    process: process,
-    loaderPid: loaderPid,
+    url: url,
     request: request,
     contentType: contentType,
     width: attrs.width,
@@ -172,76 +165,31 @@ proc newBuffer*(forkserver: ForkServer, config: BufferConfig,
     pos: CursorPosition(
       setx: -1
     ),
-    canreinterpret: canreinterpret
-  )
-
-proc newBufferFrom*(forkserver: ForkServer, attrs: WindowAttributes,
-    container: Container, contentTypeOverride: string): Container =
-  let request = newRequest(container.request.url, fromcache = true)
-  let config = container.config
-  let loaderPid = container.loaderPid
-  let bufferPid = forkserver.forkBufferWithLoader(request, config, attrs,
-    loaderPid)
-  return Container(
-    url: request.url,
-    request: request,
-    width: container.width,
-    height: container.height,
-    title: container.title,
-    config: config,
-    process: bufferPid,
-    loaderPid: loaderPid,
-    pos: CursorPosition(
-      setx: -1
-    ),
-    canreinterpret: true,
-    contentType: some(contentTypeOverride)
+    canreinterpret: canreinterpret,
+    loadinfo: "Connecting to " & request.url.host & "...",
+    cacheId: cacheId
   )
 
-func location*(container: Container): URL {.jsfget.} =
+func location(container: Container): URL {.jsfget.} =
   return container.url
 
-proc clone*(container: Container, newurl: URL): Promise[Container] =
+proc clone*(container: Container; newurl: URL): Promise[Container] =
   let url = if newurl != nil:
     newurl
   else:
-    container.location
-  return container.iface.clone(url).then(proc(pid: Pid): Container =
+    container.url
+  return container.iface.clone(url).then(proc(pid: int): Container =
     if pid == -1:
       return nil
-    return Container(
-      url: url,
-      config: container.config,
-      iface: container.iface, # changed later in setStream
-      width: container.width,
-      height: container.height,
-      title: container.title,
-      hoverText: container.hoverText,
-      lastPeek: container.lastPeek,
-      request: container.request,
-      pos: container.pos,
-      bpos: container.bpos,
-      highlights: container.highlights,
-      process: pid,
-      loaderPid: container.loaderPid,
-      loadinfo: container.loadinfo,
-      lines: container.lines,
-      lineshift: container.lineshift,
-      numLines: container.numLines,
-      code: container.code,
-      retry: container.retry,
-      hlon: container.hlon,
-      #needslines: container.needslines,
-      loadState: container.loadState,
-      events: container.events,
-      startpos: container.startpos,
-      hasstart: container.hasstart,
-      redirectdepth: container.redirectdepth,
-      select: container.select,
-      canreinterpret: container.canreinterpret,
-      ishtml: container.ishtml,
-      cloned: true
-    )
+    let nc = Container()
+    nc[] = container[]
+    nc.url = url
+    nc.process = pid
+    nc.cloned = true
+    nc.retry = @[]
+    nc.parent = nil
+    nc.children = @[]
+    return nc
   )
 
 func lineLoaded(container: Container, y: int): bool =
@@ -355,7 +303,7 @@ func maxScreenWidth(container: Container): int =
 func getTitle*(container: Container): string {.jsfunc.} =
   if container.title != "":
     return container.title
-  return container.location.serialize(excludepassword = true)
+  return container.url.serialize(excludepassword = true)
 
 func currentLineWidth(container: Container): int =
   if container.numLines == 0: return 0
@@ -472,12 +420,9 @@ proc setNumLines(container: Container, lines: int, finish = false) =
 
 proc cursorLastLine*(container: Container)
 
-proc requestLines(container: Container): EmptyPromise
-    {.discardable.} =
+proc requestLines(container: Container): EmptyPromise {.discardable.} =
   if container.iface == nil:
-    let res = EmptyPromise()
-    res.resolve()
-    return res
+    return newResolvedPromise()
   let w = container.lineWindow
   return container.iface.getLines(w).then(proc(res: GetLinesResult) =
     container.lines.setLen(w.len)
@@ -491,7 +436,7 @@ proc requestLines(container: Container): EmptyPromise
     if res.numLines != container.numLines:
       container.setNumLines(res.numLines, true)
       if container.loadState != lsLoading:
-        container.triggerEvent(STATUS)
+        container.triggerEvent(cetStatus)
     if res.numLines > 0:
       container.updateCursor()
       if container.tailOnLoad:
@@ -499,20 +444,22 @@ proc requestLines(container: Container): EmptyPromise
         container.cursorLastLine()
     let cw = container.fromy ..< container.fromy + container.height
     if w.a in cw or w.b in cw or cw.a in w or cw.b in w or isBgNew:
-      container.triggerEvent(UPDATE)
+      container.triggerEvent(cetUpdate)
   )
 
 proc redraw(container: Container) {.jsfunc.} =
-  container.triggerEvent(ContainerEvent(t: UPDATE, force: true))
+  container.triggerEvent(ContainerEvent(t: cetUpdate, force: true))
 
 proc sendCursorPosition*(container: Container) =
+  if container.iface == nil:
+    return
   container.iface.updateHover(container.cursorx, container.cursory)
       .then(proc(res: UpdateHoverResult) =
     if res.hover.len > 0:
       assert res.hover.high <= int(HoverType.high)
       for (ht, s) in res.hover:
         container.hoverText[ht] = s
-      container.triggerEvent(STATUS)
+      container.triggerEvent(cetStatus)
     if res.repaint:
       container.needslines = true
   )
@@ -521,7 +468,7 @@ proc setFromY(container: Container, y: int) {.jsfunc.} =
   if container.pos.fromy != y:
     container.pos.fromy = max(min(y, container.maxfromy), 0)
     container.needslines = true
-    container.triggerEvent(UPDATE)
+    container.triggerEvent(cetUpdate)
 
 proc setFromX(container: Container, x: int, refresh = true) {.jsfunc.} =
   if container.pos.fromx != x:
@@ -530,7 +477,7 @@ proc setFromX(container: Container, x: int, refresh = true) {.jsfunc.} =
       container.pos.cursorx = min(container.pos.fromx, container.currentLineWidth())
       if refresh:
         container.sendCursorPosition()
-    container.triggerEvent(UPDATE)
+    container.triggerEvent(cetUpdate)
 
 proc setFromXY(container: Container, x, y: int) {.jsfunc.} =
   container.setFromY(y)
@@ -580,7 +527,7 @@ proc setCursorX(container: Container, x: int, refresh = true, save = true)
   if container.cursorx == x and container.currentSelection != nil and
       container.currentSelection.x2 != x:
     container.currentSelection.x2 = x
-    container.triggerEvent(UPDATE)
+    container.triggerEvent(cetUpdate)
   if refresh:
     container.sendCursorPosition()
   if save:
@@ -602,7 +549,7 @@ proc setCursorY(container: Container, y: int, refresh = true) {.jsfunc.} =
       container.setFromY(y)
     container.pos.cursory = y
   if container.currentSelection != nil and container.currentSelection.y2 != y:
-    container.triggerEvent(UPDATE)
+    container.triggerEvent(cetUpdate)
     container.currentSelection.y2 = y
   container.restoreCursorX()
   if refresh:
@@ -1053,7 +1000,7 @@ proc scrollLeft*(container: Container, n = 1) {.jsfunc.} =
     container.setFromX(x)
 
 proc alert(container: Container, msg: string) =
-  container.triggerEvent(ContainerEvent(t: ALERT, msg: msg))
+  container.triggerEvent(ContainerEvent(t: cetAlert, msg: msg))
 
 proc lineInfo(container: Container) {.jsfunc.} =
   container.alert("line " & $(container.cursory + 1) & "/" &
@@ -1080,7 +1027,7 @@ proc gotoLine*[T: string|int](container: Container, s: T) =
     elif s[0] == '$':
       container.cursorLastLine()
     else:
-      let i = parseUInt32(s)
+      let i = parseUInt32(s, allowSign = true)
       if i.isSome and i.get > 0:
         container.markPos0()
         container.setCursorY(int(i.get - 1))
@@ -1113,6 +1060,8 @@ proc copyCursorPos*(container, c2: Container) =
   container.hasstart = true
 
 proc cursorNextLink*(container: Container, n = 1) {.jsfunc.} =
+  if container.iface == nil:
+    return
   container.markPos0()
   container.iface
     .findNextLink(container.cursorx, container.cursory, n)
@@ -1123,6 +1072,8 @@ proc cursorNextLink*(container: Container, n = 1) {.jsfunc.} =
     )
 
 proc cursorPrevLink*(container: Container, n = 1) {.jsfunc.} =
+  if container.iface == nil:
+    return
   container.markPos0()
   container.iface
     .findPrevLink(container.cursorx, container.cursory, n)
@@ -1133,6 +1084,8 @@ proc cursorPrevLink*(container: Container, n = 1) {.jsfunc.} =
     )
 
 proc cursorNextParagraph*(container: Container, n = 1) {.jsfunc.} =
+  if container.iface == nil:
+    return
   container.markPos0()
   container.iface
     .findNextParagraph(container.cursory, n)
@@ -1142,6 +1095,8 @@ proc cursorNextParagraph*(container: Container, n = 1) {.jsfunc.} =
     )
 
 proc cursorPrevParagraph*(container: Container, n = 1) {.jsfunc.} =
+  if container.iface == nil:
+    return
   container.markPos0()
   container.iface
     .findPrevParagraph(container.cursory, n)
@@ -1156,17 +1111,17 @@ proc setMark*(container: Container, id: string, x = none(int),
   let y = y.get(container.cursory)
   container.marks.withValue(id, p):
     p[] = (x, y)
-    container.triggerEvent(UPDATE)
+    container.triggerEvent(cetUpdate)
     return false
   do:
     container.marks[id] = (x, y)
-    container.triggerEvent(UPDATE)
+    container.triggerEvent(cetUpdate)
     return true
 
 proc clearMark*(container: Container, id: string): bool {.jsfunc.} =
   result = id in container.marks
   container.marks.del(id)
-  container.triggerEvent(UPDATE)
+  container.triggerEvent(cetUpdate)
 
 proc getMarkPos(container: Container, id: string): Opt[PagePos] {.jsfunc.} =
   if id == "`" or id == "'":
@@ -1226,6 +1181,8 @@ proc findPrevMark*(container: Container, x = none(int), y = none(int)):
   return bestid
 
 proc cursorNthLink*(container: Container, n = 1) {.jsfunc.} =
+  if container.iface == nil:
+    return
   container.iface
     .findNthLink(n)
     .then(proc(res: tuple[x, y: int]) =
@@ -1233,6 +1190,8 @@ proc cursorNthLink*(container: Container, n = 1) {.jsfunc.} =
         container.setCursorXYCenter(res.x, res.y))
 
 proc cursorRevNthLink*(container: Container, n = 1) {.jsfunc.} =
+  if container.iface == nil:
+    return
   container.iface
     .findRevNthLink(n)
     .then(proc(res: tuple[x, y: int]) =
@@ -1258,12 +1217,12 @@ proc onMatch(container: Container, res: BufferMatch, refresh: bool) =
         y2: res.y
       )
       container.highlights.add(hl)
-      container.triggerEvent(UPDATE)
+      container.triggerEvent(cetUpdate)
       container.hlon = false
       container.needslines = true
   elif container.hlon:
     container.clearSearchHighlights()
-    container.triggerEvent(UPDATE)
+    container.triggerEvent(cetUpdate)
     container.needslines = true
     container.hlon = false
 
@@ -1275,6 +1234,8 @@ proc cursorNextMatch*(container: Container, regex: Regex, wrap, refresh: bool,
       container.select.cursorNextMatch(regex, wrap)
     return newResolvedPromise()
   else:
+    if container.iface == nil:
+      return
     return container.iface
       .findNextMatch(regex, container.cursorx, container.cursory, wrap, n)
       .then(proc(res: BufferMatch) =
@@ -1288,6 +1249,8 @@ proc cursorPrevMatch*(container: Container, regex: Regex, wrap, refresh: bool,
       container.select.cursorPrevMatch(regex, wrap)
     return newResolvedPromise()
   else:
+    if container.iface == nil:
+      return
     container.markPos0()
     return container.iface
       .findPrevMatch(regex, container.cursorx, container.cursory, wrap, n)
@@ -1321,13 +1284,15 @@ proc cursorToggleSelection(container: Container, n = 1,
     )
     container.highlights.add(hl)
     container.currentSelection = hl
-  container.triggerEvent(UPDATE)
+  container.triggerEvent(cetUpdate)
   return container.currentSelection
 
 #TODO I don't like this API
 # maybe make selection a subclass of highlight?
 proc getSelectionText(container: Container, hl: Highlight = nil):
     Promise[string] {.jsfunc.} =
+  if container.iface == nil:
+    return
   let hl = if hl == nil: container.currentSelection else: hl
   if hl.t != HL_SELECT:
     let p = newPromise[string]()
@@ -1370,7 +1335,7 @@ proc getSelectionText(container: Container, hl: Highlight = nil):
 
 proc setLoadInfo(container: Container, msg: string) =
   container.loadinfo = msg
-  container.triggerEvent(STATUS)
+  container.triggerEvent(cetSetLoadInfo)
 
 #TODO this should be called with a timeout.
 proc onload*(container: Container, res: int) =
@@ -1382,15 +1347,15 @@ proc onload*(container: Container, res: int) =
   elif res == -1:
     container.loadState = lsLoaded
     container.setLoadInfo("")
-    container.triggerEvent(STATUS)
+    container.triggerEvent(cetStatus)
     container.needslines = true
-    container.triggerEvent(LOADED)
+    container.triggerEvent(cetLoaded)
     container.iface.getTitle().then(proc(title: string) =
       if title != "":
         container.title = title
-        container.triggerEvent(TITLE)
+        container.triggerEvent(cetTitle)
     )
-    if not container.hasstart and container.location.anchor != "":
+    if not container.hasstart and container.url.anchor != "":
       container.iface.gotoAnchor().then(proc(res: Opt[tuple[x, y: int]]) =
         if res.isSome:
           let res = res.get
@@ -1403,72 +1368,62 @@ proc onload*(container: Container, res: int) =
       container.onload(res)
     )
 
-proc load(container: Container) =
-  container.setLoadInfo("Connecting to " & container.location.host & "...")
-  container.iface.connect().then(proc(res: ConnectResult) =
-    let info = container.loadinfo
-    if not res.invalid:
-      container.code = res.code
-      if res.code == 0:
-        container.triggerEvent(SUCCESS)
-        # accept cookies
-        let cookiejar = container.config.loaderConfig.cookiejar
-        if res.cookies.len > 0 and cookiejar != nil:
-          cookiejar.add(res.cookies)
-        # set referrer policy, if any
-        if res.referrerPolicy.isSome and container.config.referer_from:
-          container.config.referrerPolicy = res.referrerPolicy.get
-        container.setLoadInfo("Connected to " & $container.location &
-          ". Downloading...")
-        if res.needsAuth:
-          container.triggerEvent(NEEDS_AUTH)
-        if res.redirect != nil:
-          container.triggerEvent(ContainerEvent(t: REDIRECT, request: res.redirect))
-        container.charset = res.charset
-        if container.contentType.isNone:
-          if res.contentType == "application/octet-stream":
-            let contentType = guessContentType(container.location.pathname,
-              "application/octet-stream", container.config.mimeTypes)
-            if contentType != "application/octet-stream":
-              container.contentType = some(contentType)
-            else:
-              container.contentType = some(res.contentType)
-          else:
-            container.contentType = some(res.contentType)
-        container.ishtml = container.contentType.get == "text/html"
-        container.triggerEvent(CHECK_MAILCAP)
+proc extractCookies(response: Response): seq[Cookie] =
+  result = @[]
+  if "Set-Cookie" in response.headers.table:
+    for s in response.headers.table["Set-Cookie"]:
+      let cookie = newCookie(s, response.url)
+      if cookie.isOk:
+        result.add(cookie.get)
+
+proc extractReferrerPolicy(response: Response): Option[ReferrerPolicy] =
+  if "Referrer-Policy" in response.headers:
+    return getReferrerPolicy(response.headers["Referrer-Policy"])
+  return none(ReferrerPolicy)
+
+# Apply data received in response.
+# Note: pager must call this before checkMailcap.
+proc applyResponse*(container: Container; response: Response) =
+  container.code = response.res
+  # accept cookies
+  let cookieJar = container.config.loaderConfig.cookieJar
+  if cookieJar != nil:
+    cookieJar.add(response.extractCookies())
+  # set referrer policy, if any
+  let referrerPolicy = response.extractReferrerPolicy()
+  if container.config.referer_from:
+    if referrerPolicy.isSome:
+      container.config.referrerPolicy = referrerPolicy.get
+  else:
+    container.config.referrerPolicy = NO_REFERRER
+  container.setLoadInfo("Connected to " & $response.url & ". Downloading...")
+  # setup content type; note that isSome means an override so we skip it
+  if container.contentType.isNone:
+    if response.contentType == "application/octet-stream":
+      let contentType = guessContentType(container.url.pathname,
+        "application/octet-stream", container.config.mimeTypes)
+      if contentType != "application/octet-stream":
+        container.contentType = some(contentType)
       else:
-        if res.errorMessage != "":
-          container.errorMessage = res.errorMessage
-        else:
-          container.errorMessage = getLoaderErrorMessage(res.code)
-        container.setLoadInfo("")
-        container.triggerEvent(FAIL)
+        container.contentType = some(response.contentType)
     else:
-      container.setLoadInfo(info)
-  )
-
-proc startload*(container: Container) =
-  container.iface.load().then(proc(res: int) =
-    container.onload(res)
-  )
-
-proc connect2*(container: Container): EmptyPromise =
-  return container.iface.connect2(container.ishtml)
-
-proc redirectToFd*(container: Container, fdin: FileHandle, wait, cache: bool):
-    EmptyPromise =
-  return container.iface.redirectToFd(fdin, wait, cache)
-
-proc readFromFd*(container: Container, fdout: FileHandle, id: string,
-    ishtml: bool): EmptyPromise =
-  container.ishtml = ishtml
-  let url = newURL("stream:" & id).get
-  container.loaderPid.passFd(url.host, fdout)
-  return container.iface.readFromFd(url, ishtml)
-
-proc quit*(container: Container) =
-  container.triggerEvent(QUIT)
+      container.contentType = some(response.contentType)
+  # setup charsets:
+  # * override charset
+  # * network charset
+  # * default charset guesses
+  # HTML may override the last two (but not the override charset).
+  if container.config.charsetOverride != CHARSET_UNKNOWN:
+    container.charsetStack = @[container.config.charsetOverride]
+  elif response.charset != CHARSET_UNKNOWN:
+    container.charsetStack = @[response.charset]
+  else:
+    container.charsetStack = @[]
+    for i in countdown(container.config.charsets.high, 0):
+      container.charsetStack.add(container.config.charsets[i])
+    if container.charsetStack.len == 0:
+      container.charsetStack.add(DefaultCharset)
+  container.charset = container.charsetStack[^1]
 
 proc cancel*(container: Container) {.jsfunc.} =
   if container.select.open:
@@ -1477,26 +1432,30 @@ proc cancel*(container: Container) {.jsfunc.} =
     container.loadState = lsCanceled
     container.alert("Canceled loading")
 
-proc findAnchor*(container: Container, anchor: string) =
+proc findAnchor*(container: Container; anchor: string) =
   container.iface.findAnchor(anchor).then(proc(found: bool) =
     if found:
-      container.triggerEvent(ContainerEvent(t: ANCHOR, anchor: anchor))
+      container.triggerEvent(ContainerEvent(t: cetAnchor, anchor: anchor))
     else:
-      container.triggerEvent(NO_ANCHOR))
+      container.triggerEvent(ContainerEvent(t: cetNoAnchor, anchor: anchor))
+  )
 
 proc readCanceled*(container: Container) =
   container.iface.readCanceled().then(proc(repaint: bool) =
     if repaint:
       container.needslines = true)
 
-proc readSuccess*(container: Container, s: string) =
+proc readSuccess*(container: Container; s: string) =
   container.iface.readSuccess(s).then(proc(res: ReadSuccessResult) =
     if res.repaint:
       container.needslines = true
     if res.open.isSome:
-      container.triggerEvent(ContainerEvent(t: OPEN, request: res.open.get)))
+      container.triggerEvent(ContainerEvent(t: cetOpen, request: res.open.get))
+  )
 
 proc reshape(container: Container): EmptyPromise {.jsfunc.} =
+  if container.iface == nil:
+    return
   return container.iface.forceRender().then(proc(): EmptyPromise =
     return container.requestLines()
   )
@@ -1509,25 +1468,25 @@ proc displaySelect(container: Container, selectResult: SelectResult) =
       container.onclick(res))
   container.select.initSelect(selectResult, container.acursorx,
     container.acursory, container.height, submitSelect)
-  container.triggerEvent(UPDATE)
+  container.triggerEvent(cetUpdate)
 
-proc onclick(container: Container, res: ClickResult) =
+proc onclick(container: Container; res: ClickResult) =
   if res.repaint:
     container.needslines = true
   if res.open.isSome:
-    container.triggerEvent(ContainerEvent(t: OPEN, request: res.open.get))
+    container.triggerEvent(ContainerEvent(t: cetOpen, request: res.open.get))
   if res.select.isSome:
     container.displaySelect(res.select.get)
   if res.readline.isSome:
     let rl = res.readline.get
     let event = if rl.area:
       ContainerEvent(
-        t: READ_AREA,
+        t: cetReadArea,
         tvalue: rl.value
       )
     else:
       ContainerEvent(
-        t: READ_LINE,
+        t: cetReadLine,
         prompt: rl.prompt,
         value: rl.value,
         password: rl.hide
@@ -1538,6 +1497,8 @@ proc click*(container: Container) {.jsfunc.} =
   if container.select.open:
     container.select.click()
   else:
+    if container.iface == nil:
+      return
     container.iface.click(container.cursorx, container.cursory)
       .then(proc(res: ClickResult) = container.onclick(res))
 
@@ -1545,12 +1506,13 @@ proc windowChange*(container: Container, attrs: WindowAttributes) =
   if attrs.width != container.width or attrs.height - 1 != container.height:
     container.width = attrs.width
     container.height = attrs.height - 1
-    container.iface.windowChange(attrs).then(proc() =
-      container.needslines = true
-    )
+    if container.iface != nil:
+      container.iface.windowChange(attrs).then(proc() =
+        container.needslines = true
+      )
 
 proc peek(container: Container) {.jsfunc.} =
-  container.alert($container.location)
+  container.alert($container.url)
 
 proc clearHover*(container: Container) =
   container.lastPeek = low(HoverType)
@@ -1582,17 +1544,24 @@ proc handleCommand(container: Container) =
   container.iface.stream.sread(packetid)
   container.iface.resolve(packetid, len - slen(packetid))
 
-proc setStream*(container: Container, stream: SocketStream,
+proc setStream*(container: Container; stream: SocketStream;
+    registerFun: proc(fd: int); fd: FileHandle; outCacheId: int) =
+  assert not container.cloned
+  container.iface = newBufferInterface(stream, registerFun)
+  container.iface.passFd(fd, outCacheId)
+  discard close(fd)
+  discard container.iface.load().then(proc(res: int) =
+    container.onload(res)
+  )
+
+proc setCloneStream*(container: Container; stream: SocketStream;
     registerFun: proc(fd: int)) =
-  if not container.cloned:
-    container.iface = newBufferInterface(stream, registerFun)
-    container.load()
-  else:
-    container.iface = cloneInterface(stream, registerFun)
-    # Maybe we have to resume loading. Let's try.
-    discard container.iface.load().then(proc(res: int) =
-      container.onload(res)
-    )
+  assert container.cloned
+  container.iface = cloneInterface(stream, registerFun)
+  # Maybe we have to resume loading. Let's try.
+  discard container.iface.load().then(proc(res: int) =
+    container.onload(res)
+  )
 
 proc onreadline(container: Container, w: Slice[int],
     handle: (proc(line: SimpleFlexibleLine)), res: GetLinesResult) =
diff --git a/src/local/pager.nim b/src/local/pager.nim
index a9d06567..51c2f89f 100644
--- a/src/local/pager.nim
+++ b/src/local/pager.nim
@@ -23,11 +23,14 @@ import extern/stdio
 import extern/tempfile
 import io/posixstream
 import io/promise
+import io/socketstream
+import io/urlfilter
 import js/error
 import js/javascript
 import js/jstypes
 import js/regex
 import js/tojs
+import loader/headers
 import loader/loader
 import loader/request
 import local/container
@@ -38,6 +41,7 @@ import types/cell
 import types/color
 import types/cookie
 import types/opt
+import types/referrer
 import types/urimethodmap
 import types/url
 import utils/strwidth
@@ -50,8 +54,20 @@ type
     NO_LINEMODE, LOCATION, USERNAME, PASSWORD, COMMAND, BUFFER, SEARCH_F,
     SEARCH_B, ISEARCH_F, ISEARCH_B, GOTO_LINE
 
+  # fdin is the original fd; fdout may be the same, or different if mailcap
+  # is used.
+  ProcMapItem = object
+    container*: Container
+    fdin*: FileHandle
+    fdout*: FileHandle
+    istreamOutputId*: int
+    ostreamOutputId*: int
+
+  PagerAlertState = enum
+    pasNormal, pasAlertOn, pasLoadInfo
+
   Pager* = ref object
-    alerton: bool
+    alertState: PagerAlertState
     alerts: seq[string]
     askcharpromise*: Promise[string]
     askcursor: int
@@ -60,23 +76,26 @@ type
     cgiDir*: seq[string]
     commandMode {.jsget.}: bool
     config: Config
+    connectingBuffers*: seq[tuple[container: Container; stream: SocketStream]]
     container*: Container
     cookiejars: Table[string, CookieJar]
+    devRandom: PosixStream
     display: FixedGrid
-    forkserver: ForkServer
+    forkserver*: ForkServer
     inputBuffer*: string # currently uninterpreted characters
     iregex: Result[Regex, string]
     isearchpromise: EmptyPromise
     lineedit*: Option[LineEdit]
     linehist: array[LineMode, LineHistory]
     linemode: LineMode
+    loader*: FileLoader
     mailcap: Mailcap
     mimeTypes: MimeTypes
     notnum*: bool # has a non-numeric character been input already?
     numload*: int
     omnirules: seq[OmniRule]
     precnum*: int32 # current number prefix (when vi-numeric-prefix is true)
-    procmap*: Table[Pid, Container]
+    procmap*: seq[ProcMapItem]
     proxy: URL
     redraw*: bool
     regex: Opt[Regex]
@@ -85,8 +104,8 @@ type
     siteconf: seq[SiteConfig]
     statusgrid*: FixedGrid
     term*: Terminal
-    tmpdir: string
-    unreg*: seq[(Pid, PosixStream)]
+    tmpdir*: string
+    unreg*: seq[tuple[pid: int; stream: PosixStream]]
     urimethodmap: URIMethodMap
     username: string
 
@@ -94,6 +113,9 @@ jsDestructor(Pager)
 
 func attrs(pager: Pager): WindowAttributes = pager.term.attrs
 
+func loaderPid(pager: Pager): int64 {.jsfget.} =
+  int64(pager.loader.process)
+
 func getRoot(container: Container): Container =
   var c = container
   while c.parent != nil: c = c.parent
@@ -237,7 +259,7 @@ proc setPaths(pager: Pager): Err[string] =
   pager.cgiDir = cgiDir
   return ok()
 
-proc newPager*(config: Config, forkserver: ForkServer, ctx: JSContext): Pager =
+proc newPager*(config: Config; forkserver: ForkServer; ctx: JSContext): Pager =
   let (mailcap, errs) = config.getMailcap()
   let pager = Pager(
     config: config,
@@ -261,6 +283,29 @@ proc newPager*(config: Config, forkserver: ForkServer, ctx: JSContext): Pager =
     pager.alert("Error reading mailcap: " & err)
   return pager
 
+proc genClientKey(pager: Pager): ClientKey =
+  var key: ClientKey
+  let n = pager.devRandom.recvData(addr key[0], key.len)
+  doAssert n == key.len
+  return key
+
+proc addLoaderClient*(pager: Pager, pid: int, config: LoaderClientConfig):
+    ClientKey =
+  var key = pager.genClientKey()
+  while unlikely(not pager.loader.addClient(key, pid, config)):
+    key = pager.genClientKey()
+  return key
+
+proc setLoader*(pager: Pager, loader: FileLoader) =
+  pager.devRandom = newPosixStream("/dev/urandom", O_RDONLY, 0)
+  pager.loader = loader
+  let config = LoaderClientConfig(
+    defaultHeaders: pager.config.getDefaultHeaders(),
+    proxy: pager.config.getProxy(),
+    filter: newURLFilter(default = true),
+  )
+  loader.key = pager.addLoaderClient(pager.loader.clientPid, config)
+
 proc launchPager*(pager: Pager, infile: File) =
   case pager.term.start(infile)
   of tsrSuccess: discard
@@ -325,17 +370,13 @@ proc refreshStatusMsg*(pager: Pager) =
     pager.writeStatusMessage($pager.precnum & pager.inputBuffer)
   elif pager.inputBuffer != "":
     pager.writeStatusMessage(pager.inputBuffer)
-  elif container.loadinfo != "":
-    pager.alerton = true
-    pager.writeStatusMessage(container.loadinfo)
-    container.loadinfo = ""
   elif pager.alerts.len > 0:
-    pager.alerton = true
+    pager.alertState = pasAlertOn
     pager.writeStatusMessage(pager.alerts[0])
     pager.alerts.delete(0)
   else:
     var format = Format(flags: {FLAG_REVERSE})
-    pager.alerton = false
+    pager.alertState = pasNormal
     container.clearHover()
     var msg = $(container.cursory + 1) & "/" & $container.numLines & " (" &
               $container.atPercentOf() & "%)"
@@ -351,8 +392,12 @@ proc refreshStatusMsg*(pager: Pager) =
       pager.writeStatusMessage(hover2, format, tw)
 
 # Call refreshStatusMsg if no alert is being displayed on the screen.
+# Alerts take precedence over load info, but load info is preserved when no
+# pending alerts exist.
 proc showAlerts*(pager: Pager) =
-  if not pager.alerton and pager.inputBuffer == "" and pager.precnum == 0:
+  if (pager.alertState == pasNormal or
+      pager.alertState == pasLoadInfo and pager.alerts.len > 0) and
+      pager.inputBuffer == "" and pager.precnum == 0:
     pager.refreshStatusMsg()
 
 proc drawBuffer*(pager: Pager, container: Container, ostream: Stream) =
@@ -444,41 +489,90 @@ proc fulfillCharAsk*(pager: Pager, s: string) =
   pager.askcharpromise = nil
   pager.askprompt = ""
 
-proc registerContainer*(pager: Pager, container: Container) =
-  pager.procmap[container.process] = container
-
 proc addContainer*(pager: Pager, container: Container) =
   container.parent = pager.container
   if pager.container != nil:
     pager.container.children.insert(container, 0)
-  pager.registerContainer(container)
   pager.setContainer(container)
 
-proc newBuffer(pager: Pager, bufferConfig: BufferConfig, request: Request,
-    title = "", redirectdepth = 0, canreinterpret = true, fd = FileHandle(-1),
-    contentType = none(string)): Container =
-  return newBuffer(
-    pager.forkserver,
+proc onSetLoadInfo(pager: Pager; container: Container) =
+  if pager.alertState != pasAlertOn:
+    if container.loadinfo == "":
+      pager.alertState = pasNormal
+    else:
+      pager.writeStatusMessage(container.loadinfo)
+      pager.alertState = pasLoadInfo
+
+proc newContainer(pager: Pager; bufferConfig: BufferConfig; request: Request;
+    title = ""; redirectdepth = 0; canreinterpret = true;
+    contentType = none(string); charsetStack: seq[Charset] = @[];
+    url: URL = request.url; cacheId = -1): Container =
+  request.suspended = true
+  if bufferConfig.loaderConfig.cookieJar != nil:
+    # loader stores cookie jars per client, but we have no client yet.
+    # therefore we must set cookie here
+    let cookie = bufferConfig.loaderConfig.cookieJar.serialize(request.url)
+    if cookie != "":
+      request.headers["Cookie"] = cookie
+  if request.referrer != nil:
+    # same with referrer
+    let r = request.referrer.getReferrer(request.url,
+      bufferConfig.referrerPolicy)
+    if r != "":
+      request.headers["Referer"] = r
+  let stream = pager.loader.startRequest(request)
+  pager.loader.registerFun(stream.fd)
+  let container = newContainer(
     bufferConfig,
+    url,
     request,
     pager.term.attrs,
     title,
     redirectdepth,
     canreinterpret,
-    fd,
-    contentType
+    contentType,
+    charsetStack,
+    cacheId
   )
+  pager.connectingBuffers.add((container, stream))
+  pager.onSetLoadInfo(container)
+  return container
+
+proc newContainerFrom(pager: Pager; container: Container; contentType: string):
+    Container =
+  let url = newURL("cache:" & $container.cacheId).get
+  return pager.newContainer(
+    container.config,
+    newRequest(url),
+    contentType = some(contentType),
+    charsetStack = container.charsetStack,
+    url = container.url,
+    cacheId = container.cacheId
+  )
+
+func findConnectingBuffer*(pager: Pager; fd: int): int =
+  for i, (_, stream) in pager.connectingBuffers:
+    if stream.fd == fd:
+      return i
+  -1
 
-proc dupeBuffer(pager: Pager, container: Container, location: URL) =
-  container.clone(location).then(proc(container: Container) =
+proc dupeBuffer(pager: Pager, container: Container, url: URL) =
+  container.clone(url).then(proc(container: Container) =
     if container == nil:
       pager.alert("Failed to duplicate buffer.")
     else:
       pager.addContainer(container)
+      pager.procmap.add(ProcMapItem(
+        container: container,
+        fdin: -1,
+        fdout: -1,
+        istreamOutputId: -1,
+        ostreamOutputId: -1
+      ))
   )
 
 proc dupeBuffer(pager: Pager) {.jsfunc.} =
-  pager.dupeBuffer(pager.container, pager.container.location)
+  pager.dupeBuffer(pager.container, pager.container.url)
 
 # The prevBuffer and nextBuffer procedures emulate w3m's PREV and NEXT
 # commands by traversing the container tree in a depth-first order.
@@ -583,8 +677,10 @@ proc deleteContainer(pager: Pager, container: Container) =
       pager.setContainer(nil)
   container.parent = nil
   container.children.setLen(0)
-  pager.unreg.add((container.process, container.iface.stream))
-  pager.forkserver.removeChild(container.process)
+  if container.iface != nil:
+    pager.unreg.add((container.process, container.iface.stream))
+    pager.forkserver.removeChild(container.process)
+    pager.loader.removeClient(container.process)
 
 proc discardBuffer*(pager: Pager, container = none(Container)) {.jsfunc.} =
   let c = container.get(pager.container)
@@ -607,19 +703,17 @@ proc toggleSource(pager: Pager) {.jsfunc.} =
   if pager.container.sourcepair != nil:
     pager.setContainer(pager.container.sourcepair)
   else:
-    let contentType = if pager.container.ishtml:
-      "text/plain"
-    else:
+    let ishtml = not pager.container.ishtml
+    #TODO I wish I could set the contentType to whatever I wanted, not just HTML
+    let contentType = if ishtml:
       "text/html"
-    let container = newBufferFrom(
-      pager.forkserver,
-      pager.attrs,
-      pager.container,
-      contentType
-    )
-    container.sourcepair = pager.container
-    pager.container.sourcepair = container
-    pager.addContainer(container)
+    else:
+      "text/plain"
+    let container = pager.newContainerFrom(pager.container, contentType)
+    if container != nil:
+      container.sourcepair = pager.container
+      pager.container.sourcepair = container
+      pager.addContainer(container)
 
 proc windowChange*(pager: Pager) =
   let oldAttrs = pager.attrs
@@ -639,13 +733,13 @@ proc windowChange*(pager: Pager) =
 
 # Apply siteconf settings to a request.
 # Note that this may modify the URL passed.
-proc applySiteconf(pager: Pager, url: var URL): BufferConfig =
+proc applySiteconf(pager: Pager; url: var URL; cs: Charset): BufferConfig =
   let host = url.host
-  var referer_from: bool
-  var cookiejar: CookieJar
+  var referer_from = false
+  var cookieJar: CookieJar = nil
   var headers = pager.config.getDefaultHeaders()
-  var scripting: bool
-  var images: bool
+  var scripting = false
+  var images = false
   var charsets = pager.config.encoding.document_charset
   var userstyle = pager.config.css.stylesheet
   var proxy = pager.proxy
@@ -667,9 +761,9 @@ proc applySiteconf(pager: Pager, url: var URL): BufferConfig =
         if jarid notin pager.cookiejars:
           pager.cookiejars[jarid] = newCookieJar(url,
             sc.third_party_cookie)
-        cookiejar = pager.cookiejars[jarid]
+        cookieJar = pager.cookiejars[jarid]
       else:
-        cookiejar = nil # override
+        cookieJar = nil # override
     if sc.scripting.isSome:
       scripting = sc.scripting.get
     if sc.referer_from.isSome:
@@ -683,18 +777,17 @@ proc applySiteconf(pager: Pager, url: var URL): BufferConfig =
       userstyle &= sc.stylesheet.get
     if sc.proxy.isSome:
       proxy = sc.proxy.get
-  return pager.config.getBufferConfig(url, cookiejar, headers, referer_from,
+  return pager.config.getBufferConfig(url, cookieJar, headers, referer_from,
     scripting, charsets, images, userstyle, proxy, mimeTypes, urimethodmap,
-    pager.cgiDir, pager.tmpdir)
+    pager.cgiDir, pager.tmpdir, cs)
 
 # Load request in a new buffer.
 proc gotoURL(pager: Pager, request: Request, prevurl = none(URL),
     contentType = none(string), cs = CHARSET_UNKNOWN, replace: Container = nil,
     redirectdepth = 0, referrer: Container = nil) =
   if referrer != nil and referrer.config.referer_from:
-    request.referer = referrer.location
-  var bufferConfig = pager.applySiteconf(request.url)
-  bufferConfig.charsetOverride = cs
+    request.referrer = referrer.url
+  var bufferConfig = pager.applySiteconf(request.url, cs)
   if prevurl.isNone or not prevurl.get.equals(request.url, true) or
       request.url.hash == "" or request.httpMethod != HTTP_GET:
     # Basically, we want to reload the page *only* when
@@ -705,7 +798,7 @@ proc gotoURL(pager: Pager, request: Request, prevurl = none(URL),
     # feedback on what is actually going to happen when typing a URL; TODO.
     if referrer != nil:
       bufferConfig.referrerPolicy = referrer.config.referrerPolicy
-    let container = pager.newBuffer(
+    let container = pager.newContainer(
       bufferConfig,
       request,
       redirectdepth = redirectdepth,
@@ -714,7 +807,6 @@ proc gotoURL(pager: Pager, request: Request, prevurl = none(URL),
     if replace != nil:
       container.replace = replace
       container.copyCursorPos(container.replace)
-      pager.registerContainer(container)
     else:
       pager.addContainer(container)
     inc pager.numload
@@ -744,7 +836,7 @@ proc loadURL*(pager: Pager, url: string, ctype = none(string),
   let firstparse = parseURL(url)
   if firstparse.isSome:
     let prev = if pager.container != nil:
-      some(pager.container.location)
+      some(pager.container.url)
     else:
       none(URL)
     pager.gotoURL(newRequest(firstparse.get), prev, ctype, cs)
@@ -769,23 +861,23 @@ proc loadURL*(pager: Pager, url: string, ctype = none(string),
       pager.container.retry = urls
 
 proc readPipe0*(pager: Pager, contentType: string, cs: Charset,
-    fd: FileHandle, location: Option[URL], title: string,
-    canreinterpret: bool): Container =
-  var location = location.get(newURL("stream:-").get)
-  var bufferConfig = pager.applySiteconf(location)
-  bufferConfig.charsetOverride = cs
-  return pager.newBuffer(
+    fd: FileHandle, url: URL, title: string, canreinterpret: bool): Container =
+  var url = url
+  pager.loader.passFd(url.pathname, fd)
+  safeClose(fd)
+  let bufferConfig = pager.applySiteconf(url, cs)
+  return pager.newContainer(
     bufferConfig,
-    newRequest(location),
+    newRequest(url),
     title = title,
     canreinterpret = canreinterpret,
-    fd = fd,
     contentType = some(contentType)
   )
 
 proc readPipe*(pager: Pager, contentType: string, cs: Charset, fd: FileHandle,
     title: string) =
-  let container = pager.readPipe0(contentType, cs, fd, none(URL), title, true)
+  let url = newURL("stream:-").get
+  let container = pager.readPipe0(contentType, cs, fd, url, title, true)
   inc pager.numload
   pager.addContainer(container)
 
@@ -855,12 +947,12 @@ proc updateReadLine*(pager: Pager) =
         pager.username = lineedit.news
         pager.setLineEdit("Password: ", PASSWORD, hide = true)
       of PASSWORD:
-        let url = newURL(pager.container.location)
+        let url = newURL(pager.container.url)
         url.username = pager.username
         url.password = lineedit.news
         pager.username = ""
         pager.gotoURL(
-          newRequest(url), some(pager.container.location),
+          newRequest(url), some(pager.container.url),
           replace = pager.container,
           referrer = pager.container
         )
@@ -901,7 +993,7 @@ proc load(pager: Pager, s = "") {.jsfunc.} =
     if s.len > 1:
       pager.loadURL(s[0..^2])
   elif s == "":
-    pager.setLineEdit("URL: ", LOCATION, $pager.container.location)
+    pager.setLineEdit("URL: ", LOCATION, $pager.container.url)
   else:
     pager.setLineEdit("URL: ", LOCATION, s)
 
@@ -912,12 +1004,12 @@ proc jsGotoURL(pager: Pager, s: string): JSResult[void] {.jsfunc: "gotoURL".} =
 
 # Reload the page in a new buffer, then kill the previous buffer.
 proc reload(pager: Pager) {.jsfunc.} =
-  pager.gotoURL(newRequest(pager.container.location), none(URL),
+  pager.gotoURL(newRequest(pager.container.url), none(URL),
     pager.container.contentType, replace = pager.container)
 
 proc setEnvVars(pager: Pager) {.jsfunc.} =
   try:
-    putEnv("CHA_URL", $pager.container.location)
+    putEnv("CHA_URL", $pager.container.url)
     putEnv("CHA_CHARSET", $pager.container.charset)
   except OSError:
     pager.alert("Warning: failed to set some environment variables")
@@ -948,238 +1040,209 @@ proc externInto(pager: Pager, cmd, ins: string): bool {.jsfunc.} =
   pager.setEnvVars()
   return runProcessInto(cmd, ins)
 
-proc externFilterSource(pager: Pager, cmd: string, c: Container = nil,
+proc externFilterSource(pager: Pager; cmd: string; c: Container = nil;
     contentType = opt(string)) {.jsfunc.} =
-  let container = newBufferFrom(
-    pager.forkserver,
-    pager.attrs,
-    if c != nil: c else: pager.container,
-    contentType.get(pager.container.contentType.get(""))
-  )
+  let fromc = if c != nil: c else: pager.container
+  let contentType = contentType.get(pager.container.contentType.get(""))
+  let container = pager.newContainerFrom(fromc, contentType)
+  container.ishtml = contentType == "text/html"
   pager.addContainer(container)
   container.filter = BufferFilter(cmd: cmd)
 
 proc authorize(pager: Pager) =
   pager.setLineEdit("Username: ", USERNAME)
 
-type CheckMailcapResult = tuple[promise: EmptyPromise, connect: bool]
-
-proc checkMailcap(pager: Pager, container: Container,
-  contentTypeOverride = none(string)): CheckMailcapResult
+type CheckMailcapResult = object
+  fdout: int
+  ostreamOutputId: int
+  connect: bool
+  ishtml: bool
 
 # Pipe output of an x-ansioutput mailcap command to the text/x-ansi handler.
-proc ansiDecode(pager: Pager, container: Container, fdin: cint,
-    ishtml: var bool, fdout: var cint) =
-  let cs = container.charset
-  let url = container.location
-  let entry = pager.mailcap.getMailcapEntry("text/x-ansi", "", url, cs)
+proc ansiDecode(pager: Pager; url: URL; charset: Charset; ishtml: var bool;
+    fdin: cint): cint =
+  let entry = pager.mailcap.getMailcapEntry("text/x-ansi", "", url, charset)
   var canpipe = true
-  let cmd = unquoteCommand(entry.cmd, "text/x-ansi", "", url, cs, canpipe)
+  let cmd = unquoteCommand(entry.cmd, "text/x-ansi", "", url, charset, canpipe)
   if not canpipe:
     pager.alert("Error: could not pipe to text/x-ansi, decoding as text/plain")
+    return -1
+  var pipefdOutAnsi: array[2, cint]
+  if pipe(pipefdOutAnsi) == -1:
+    pager.alert("Error: failed to open pipe")
+    return
+  case fork()
+  of -1:
+    pager.alert("Error: failed to fork ANSI decoder process")
+    discard close(pipefdOutAnsi[0])
+    discard close(pipefdOutAnsi[1])
+    return -1
+  of 0: # child process
+    discard close(pipefdOutAnsi[0])
+    discard dup2(fdin, stdin.getFileHandle())
+    discard close(fdin)
+    discard dup2(pipefdOutAnsi[1], stdout.getFileHandle())
+    discard close(pipefdOutAnsi[1])
+    closeStderr()
+    myExec(cmd)
+    assert false
   else:
-    var pipefdOutAnsi: array[2, cint]
-    if pipe(pipefdOutAnsi) == -1:
-      raise newException(Defect, "Failed to open pipe.")
-    case fork()
-    of -1:
-      pager.alert("Error: failed to fork ANSI decoder process")
-      discard close(pipefdOutAnsi[0])
-      discard close(pipefdOutAnsi[1])
-    of 0: # child process
-      if fdin != -1:
-        discard close(fdin)
-      discard close(pipefdOutAnsi[0])
-      discard dup2(fdout, stdin.getFileHandle())
-      discard close(fdout)
-      discard dup2(pipefdOutAnsi[1], stdout.getFileHandle())
-      discard close(pipefdOutAnsi[1])
-      closeStderr()
-      myExec(cmd)
-      assert false
-    else:
-      discard close(pipefdOutAnsi[1])
-      discard close(fdout)
-      fdout = pipefdOutAnsi[0]
-      ishtml = HTMLOUTPUT in entry.flags
+    discard close(pipefdOutAnsi[1])
+    discard close(fdin)
+    ishtml = HTMLOUTPUT in entry.flags
+    return pipefdOutAnsi[0]
 
 # Pipe input into the mailcap command, then read its output into a buffer.
 # needsterminal is ignored.
-proc runMailcapReadPipe(pager: Pager, container: Container,
-    entry: MailcapEntry, cmd: string): CheckMailcapResult =
-  var pipefdIn: array[2, cint]
-  var pipefdOut: array[2, cint]
-  if pipe(pipefdIn) == -1 or pipe(pipefdOut) == -1:
-    raise newException(Defect, "Failed to open pipe.")
+proc runMailcapReadPipe(pager: Pager; stream: SocketStream; cmd: string;
+    pipefdOut: array[2, cint]): int =
   let pid = fork()
   if pid == -1:
-    pager.alert("Failed to fork process!")
-    return (nil, false)
-  elif pid == 0: # child process
-    discard close(pipefdIn[1])
+    pager.alert("Error: failed to fork mailcap read process")
+    return -1
+  elif pid == 0:
+    # child process
     discard close(pipefdOut[0])
-    discard dup2(pipefdIn[0], stdin.getFileHandle())
+    discard dup2(stream.fd, stdin.getFileHandle())
+    stream.close()
     discard dup2(pipefdOut[1], stdout.getFileHandle())
     closeStderr()
-    discard close(pipefdIn[0])
     discard close(pipefdOut[1])
     myExec(cmd)
-    assert false
-  else:
-    # parent
-    discard close(pipefdIn[0])
-    discard close(pipefdOut[1])
-    let fdin = pipefdIn[1]
-    var fdout = pipefdOut[0]
-    var ishtml = HTMLOUTPUT in entry.flags
-    if not ishtml and ANSIOUTPUT in entry.flags:
-      # decode ANSI sequence
-      pager.ansiDecode(container, fdin, ishtml, fdout)
-    let p = container.redirectToFd(fdin, wait = false, cache = true)
-    discard close(fdin)
-    let p2 = p.then(proc(): auto =
-      let p = container.readFromFd(fdout, $pid, ishtml)
-      discard close(fdout)
-      return p
-    )
-    return (p2, true)
+    doAssert false
+  # parent
+  pid
 
 # Pipe input into the mailcap command, and discard its output.
 # If needsterminal, leave stderr and stdout open and wait for the process.
-proc runMailcapWritePipe(pager: Pager, container: Container,
-    entry: MailcapEntry, cmd: string): CheckMailcapResult =
-  let needsterminal = NEEDSTERMINAL in entry.flags
-  var pipefd: array[2, cint]
-  if pipe(pipefd) == -1:
-    raise newException(Defect, "Failed to open pipe.")
+proc runMailcapWritePipe(pager: Pager; stream: SocketStream;
+    needsterminal: bool; cmd: string) =
   if needsterminal:
     pager.term.quit()
   let pid = fork()
   if pid == -1:
-    return (nil, false)
+    pager.alert("Error: failed to fork mailcap write process")
   elif pid == 0:
     # child process
-    discard close(pipefd[1])
-    discard dup2(pipefd[0], stdin.getFileHandle())
+    discard dup2(stream.fd, stdin.getFileHandle())
+    stream.close()
     if not needsterminal:
       closeStdout()
       closeStderr()
-    discard close(pipefd[0])
     myExec(cmd)
-    assert false
+    doAssert false
   else:
     # parent
-    discard close(pipefd[0])
-    let fd = pipefd[1]
-    let p = container.redirectToFd(fd, wait = true, cache = false)
-    discard close(fd)
+    stream.close()
     if needsterminal:
       var x: cint
       discard waitpid(pid, x, 0)
       pager.term.restart()
-    return (p, false)
+
+proc writeToFile(istream: SocketStream; outpath: string): bool =
+  let ps = newPosixStream(outpath, O_WRONLY or O_CREAT, 0o600)
+  if ps == nil:
+    return false
+  var buffer: array[4096, uint8]
+  while true:
+    let n = istream.recvData(buffer)
+    if n == 0:
+      break
+    if ps.sendData(buffer.toOpenArray(0, n - 1)) < n:
+      ps.close()
+      return false
+    if n < buffer.len:
+      break
+  ps.close()
+  true
 
 # Save input in a file, run the command, and redirect its output to a
 # new buffer.
 # needsterminal is ignored.
-proc runMailcapReadFile(pager: Pager, container: Container,
-    entry: MailcapEntry, cmd, outpath: string): CheckMailcapResult =
-  let fd = open(outpath, O_WRONLY or O_CREAT, 0o600)
-  if fd == -1:
-    return (nil, false)
-  let p = container.redirectToFd(fd, wait = true, cache = true).then(proc():
-      auto =
-    var pipefd: array[2, cint] # redirect stdout here
-    if pipe(pipefd) == -1:
-      raise newException(Defect, "Failed to open pipe.")
+proc runMailcapReadFile(pager: Pager; stream: SocketStream;
+    cmd, outpath: string; pipefdOut: array[2, cint]): int =
+  let pid = fork()
+  if pid == 0:
+    # child process
+    discard close(pipefdOut[0])
+    discard dup2(pipefdOut[1], stdout.getFileHandle())
+    discard close(pipefdOut[1])
+    closeStderr()
+    if not stream.writeToFile(outpath):
+      #TODO print error message
+      quit(1)
+    stream.close()
+    let ret = execCmd(cmd)
+    discard tryRemoveFile(outpath)
+    quit(ret)
+  # parent
+  pid
+
+# Save input in a file, run the command, and discard its output.
+# If needsterminal, leave stderr and stdout open and wait for the process.
+proc runMailcapWriteFile(pager: Pager; stream: SocketStream;
+    needsterminal: bool; cmd, outpath: string) =
+  if needsterminal:
+    pager.term.quit()
+    if not stream.writeToFile(outpath):
+      pager.term.restart()
+      pager.alert("Error: failed to write file for mailcap process")
+    else:
+      discard execCmd(cmd)
+      discard tryRemoveFile(outpath)
+      pager.term.restart()
+  else:
+    # don't block
     let pid = fork()
     if pid == 0:
       # child process
-      discard close(pipefd[0])
-      discard dup2(pipefd[1], stdout.getFileHandle())
-      discard close(pipefd[1])
+      closeStdin()
+      closeStdout()
       closeStderr()
+      if not stream.writeToFile(outpath):
+        #TODO print error message (maybe in parent?)
+        quit(1)
+      stream.close()
       let ret = execCmd(cmd)
       discard tryRemoveFile(outpath)
       quit(ret)
     # parent
-    discard close(pipefd[1])
-    var fdout = pipefd[0]
-    var ishtml = HTMLOUTPUT in entry.flags
-    if not ishtml and ANSIOUTPUT in entry.flags:
-      pager.ansiDecode(container, -1, ishtml, fdout)
-    let p = container.readFromFd(fdout, $pid, ishtml)
-    discard close(fdout)
-    return p
-  )
-  return (p, true)
+    stream.close()
 
-# Save input in a file, run the command, and discard its output.
-# If needsterminal, leave stderr and stdout open and wait for the process.
-proc runMailcapWriteFile(pager: Pager, container: Container,
-    entry: MailcapEntry, cmd, outpath: string): CheckMailcapResult =
-  let needsterminal = NEEDSTERMINAL in entry.flags
-  let fd = open(outpath, O_WRONLY or O_CREAT, 0o600)
-  if fd == -1:
-    return (nil, false)
-  let p = container.redirectToFd(fd, wait = true, cache = false).then(proc() =
-    if needsterminal:
-      pager.term.quit()
-      discard execCmd(cmd)
-      discard tryRemoveFile(outpath)
-      pager.term.restart()
-    else:
-      # don't block
-      let pid = fork()
-      if pid == 0:
-        # child process
-        closeStdin()
-        closeStdout()
-        closeStderr()
-        let ret = execCmd(cmd)
-        discard tryRemoveFile(outpath)
-        quit(ret)
-  )
-  return (p, false)
-
-proc filterBuffer(pager: Pager, container: Container): CheckMailcapResult =
+proc filterBuffer(pager: Pager; stream: SocketStream; cmd: string;
+    ishtml: bool): CheckMailcapResult =
   pager.setEnvVars()
-  let cmd = container.filter.cmd
-  var pipefd_in: array[2, cint]
-  if pipe(pipefd_in) == -1:
-    raise newException(Defect, "Failed to open pipe.")
   var pipefd_out: array[2, cint]
   if pipe(pipefd_out) == -1:
-    raise newException(Defect, "Failed to open pipe.")
+    pager.alert("Error: failed to open pipe")
+    return CheckMailcapResult(connect: false, fdout: -1)
   let pid = fork()
   if pid == -1:
-    return (nil, true)
+    pager.alert("Error: failed to fork buffer filter process")
+    return CheckMailcapResult(connect: false, fdout: -1)
   elif pid == 0:
     # child
-    discard close(pipefd_in[1])
     discard close(pipefd_out[0])
-    stdout.flushFile()
-    discard dup2(pipefd_in[0], stdin.getFileHandle())
+    discard dup2(stream.fd, stdin.getFileHandle())
+    stream.close()
     discard dup2(pipefd_out[1], stdout.getFileHandle())
     closeStderr()
-    discard close(pipefd_in[0])
     discard close(pipefd_out[1])
     myExec(cmd)
-    assert false
-  else:
-    # parent
-    discard close(pipefd_in[0])
-    discard close(pipefd_out[1])
-    let fdin = pipefd_in[1]
-    let fdout = pipefd_out[0]
-    let p = container.redirectToFd(fdin, wait = false, cache = false)
-    let p2 = p.then(proc(): auto =
-      discard close(fdin)
-      return container.readFromFd(fdout, $pid, container.ishtml)
-    ).then(proc() =
-      discard close(fdout)
-    )
-    return (p2, true)
+    doAssert false
+  # parent
+  discard close(pipefd_out[1])
+  let fdout = pipefd_out[0]
+  let url = parseURL("stream:" & $pid).get
+  pager.loader.passFd(url.pathname, FileHandle(fdout))
+  safeClose(fdout)
+  let response = pager.loader.doRequest(newRequest(url, suspended = true))
+  return CheckMailcapResult(
+    connect: true,
+    fdout: response.body.fd,
+    ostreamOutputId: response.outputId,
+    ishtml: ishtml
+  )
 
 # Search for a mailcap entry, and if found, execute the specified command
 # and pipeline the input and output appropriately.
@@ -1190,129 +1253,194 @@ proc filterBuffer(pager: Pager, container: Container): CheckMailcapResult =
 # * write to file, run, read stdout
 # If needsterminal is specified, and stdout is not being read, then the
 # pager is suspended until the command exits.
-#TODO add support for edit/compose, better error handling (use Promise[bool]
-# instead of tuple[EmptyPromise, bool])
-proc checkMailcap(pager: Pager, container: Container,
-    contentTypeOverride = none(string)): CheckMailcapResult =
+#TODO add support for edit/compose, better error handling
+proc checkMailcap(pager: Pager; container: Container; stream: SocketStream;
+    istreamOutputId: int): CheckMailcapResult =
   if container.filter != nil:
-    return pager.filterBuffer(container)
-  if container.contentType.isNone:
-    return (nil, true)
-  let contentType = contentTypeOverride.get(container.contentType.get)
+    return pager.filterBuffer(stream, container.filter.cmd, container.ishtml)
+  # contentType must exist, because we set it in applyResponse
+  let contentType = container.contentType.get
   if contentType == "text/html":
-    # We support HTML natively, so it would make little sense to execute
+    # We support text/html natively, so it would make little sense to execute
     # mailcap filters for it.
-    return (nil, true)
-  elif contentType == "text/plain":
-    # This could potentially be useful. Unfortunately, many mailcaps include
-    # a text/plain entry with less by default, so it's probably better to
-    # ignore this.
-    return (nil, true)
+    return CheckMailcapResult(connect: true, fdout: stream.fd, ishtml: true)
+  if contentType == "text/plain":
+    # text/plain could potentially be useful. Unfortunately, many mailcaps
+    # include a text/plain entry with less by default, so it's probably better
+    # to ignore this.
+    return CheckMailcapResult(connect: true, fdout: stream.fd)
   #TODO callback for outpath or something
-  let url = container.location
+  let url = container.url
   let cs = container.charset
   let entry = pager.mailcap.getMailcapEntry(contentType, "", url, cs)
-  if entry != nil:
-    let tmpdir = pager.tmpdir
-    let ext = container.location.pathname.afterLast('.')
-    let tempfile = getTempFile(tmpdir, ext)
-    let outpath = if entry.nametemplate != "":
-      unquoteCommand(entry.nametemplate, contentType, tempfile, url, cs)
-    else:
-      tempfile
-    var canpipe = true
-    let cmd = unquoteCommand(entry.cmd, contentType, outpath, url, cs, canpipe)
-    putEnv("MAILCAP_URL", $url) #TODO delEnv this after command is finished?
-    if {COPIOUSOUTPUT, HTMLOUTPUT, ANSIOUTPUT} * entry.flags == {}:
-      # no output.
+  if entry == nil:
+    return CheckMailcapResult(connect: true, fdout: stream.fd)
+  let tmpdir = pager.tmpdir
+  let ext = url.pathname.afterLast('.')
+  let tempfile = getTempFile(tmpdir, ext)
+  let outpath = if entry.nametemplate != "":
+    unquoteCommand(entry.nametemplate, contentType, tempfile, url, cs)
+  else:
+    tempfile
+  var canpipe = true
+  let cmd = unquoteCommand(entry.cmd, contentType, outpath, url, cs, canpipe)
+  var ishtml = HTMLOUTPUT in entry.flags
+  let needsterminal = NEEDSTERMINAL in entry.flags
+  putEnv("MAILCAP_URL", $url)
+  block needsConnect:
+    if entry.flags * {COPIOUSOUTPUT, HTMLOUTPUT, ANSIOUTPUT} == {}:
+      # No output. Resume here, so that blocking needsterminal filters work.
+      pager.loader.resume(@[istreamOutputId])
       if canpipe:
-        return pager.runMailcapWritePipe(container, entry[], cmd)
+        pager.runMailcapWritePipe(stream, needsterminal, cmd)
       else:
-        return pager.runMailcapWriteFile(container, entry[], cmd, outpath)
+        pager.runMailcapWriteFile(stream, needsterminal, cmd, outpath)
+      # stream is already closed
+      break needsConnect # never connect here, since there's no output
+    var pipefdOut: array[2, cint]
+    if pipe(pipefdOut) == -1:
+      pager.alert("Error: failed to open pipe")
+      stream.close() # connect: false implies that we consumed the stream
+      break needsConnect
+    let pid = if canpipe:
+      pager.runMailcapReadPipe(stream, cmd, pipefdOut)
     else:
-      if canpipe:
-        return pager.runMailcapReadPipe(container, entry[], cmd)
-      else:
-        return pager.runMailcapReadFile(container, entry[], cmd, outpath)
-  return (nil, true)
+      pager.runMailcapReadFile(stream, cmd, outpath, pipefdOut)
+    discard close(pipefdOut[1]) # close write
+    let fdout = if not ishtml and ANSIOUTPUT in entry.flags:
+      pager.ansiDecode(url, cs, ishtml, pipefdOut[0])
+    else:
+      pipefdOut[0]
+    delEnv("MAILCAP_URL")
+    let url = parseURL("stream:" & $pid).get
+    pager.loader.passFd(url.pathname, FileHandle(fdout))
+    safeClose(cint(fdout))
+    let response = pager.loader.doRequest(newRequest(url, suspended = true))
+    return CheckMailcapResult(
+      connect: true,
+      fdout: response.body.fd,
+      ostreamOutputId: response.outputId,
+      ishtml: ishtml
+    )
+  delEnv("MAILCAP_URL")
+  return CheckMailcapResult(connect: false, fdout: -1)
 
-proc redirectTo(pager: Pager, container: Container, request: Request) =
+proc redirectTo(pager: Pager; container: Container; request: Request) =
   pager.alert("Redirecting to " & $request.url)
-  pager.gotoURL(request, some(container.location), replace = container,
+  pager.gotoURL(request, some(container.url), replace = container,
     redirectdepth = container.redirectdepth + 1, referrer = container)
 
-proc handleEvent0(pager: Pager, container: Container, event: ContainerEvent): bool =
-  case event.t
-  of FAIL:
-    dec pager.numload
-    pager.deleteContainer(container)
-    if container.retry.len > 0:
-      pager.gotoURL(newRequest(container.retry.pop()),
-        contentType = container.contentType)
+proc fail*(pager: Pager; container: Container; errorMessage: string) =
+  dec pager.numload
+  pager.deleteContainer(container)
+  if container.retry.len > 0:
+    pager.gotoURL(newRequest(container.retry.pop()),
+      contentType = container.contentType)
+  else:
+    pager.alert("Can't load " & $container.url & " (" & errorMessage & ")")
+
+proc redirect*(pager: Pager; container: Container; response: Response) =
+  # still need to apply response, or we lose cookie jars.
+  container.applyResponse(response)
+  let request = response.redirect
+  if container.redirectdepth < pager.config.network.max_redirect:
+    if container.url.scheme == request.url.scheme or
+        container.url.scheme == "cgi-bin" or
+        container.url.scheme == "http" and request.url.scheme == "https" or
+        container.url.scheme == "https" and request.url.scheme == "http":
+      pager.redirectTo(container, request)
     else:
-      pager.alert("Can't load " & $container.location & " (" &
-        container.errorMessage & ")")
-    return false
-  of SUCCESS:
+      let url = request.url
+      pager.ask("Warning: switch protocols? " & $url).then(proc(x: bool) =
+        if x:
+          pager.redirectTo(container, request)
+      )
+  else:
+    pager.alert("Error: maximum redirection depth reached")
+    pager.deleteContainer(container)
+
+proc replace(pager: Pager; container: Container) =
+  let n = container.replace.children.find(container)
+  if n != -1:
+    container.replace.children.delete(n)
+    container.parent = nil
+  let n2 = container.children.find(container.replace)
+  if n2 != -1:
+    container.children.delete(n2)
+    container.replace.parent = nil
+  container.children.add(container.replace.children)
+  for child in container.children:
+    child.parent = container
+  container.replace.children.setLen(0)
+  if container.replace.parent != nil:
+    container.parent = container.replace.parent
+    let n = container.replace.parent.children.find(container.replace)
+    assert n != -1, "Container not a child of its parent"
+    container.parent.children[n] = container
+    container.replace.parent = nil
+  if pager.container == container.replace:
+    pager.setContainer(container)
+  pager.deleteContainer(container.replace)
+  container.replace = nil
+
+proc connected*(pager: Pager; container: Container; response: Response) =
+  let istream = response.body
+  container.applyResponse(response)
+  if response.status == 401: # unauthorized
+    pager.authorize()
+    istream.close()
+    return
+  let mailcapRes = pager.checkMailcap(container, istream, response.outputId)
+  if mailcapRes.connect:
+    container.ishtml = mailcapRes.ishtml
+    container.applyResponse(response)
+    # buffer now actually exists; create a process for it
+    container.process = pager.forkserver.forkBuffer(
+      container.config,
+      container.url,
+      container.request,
+      pager.attrs,
+      container.ishtml,
+      container.charsetStack
+    )
+    if mailcapRes.fdout != istream.fd:
+      # istream has been redirected into a filter
+      istream.close()
+    pager.procmap.add(ProcMapItem(
+      container: container, 
+      fdout: FileHandle(mailcapRes.fdout),
+      fdin: FileHandle(istream.fd),
+      ostreamOutputId: mailcapRes.ostreamOutputId,
+      istreamOutputId: response.outputId
+    ))
     if container.replace != nil:
-      let n = container.replace.children.find(container)
-      if n != -1:
-        container.replace.children.delete(n)
-        container.parent = nil
-      let n2 = container.children.find(container.replace)
-      if n2 != -1:
-        container.children.delete(n2)
-        container.replace.parent = nil
-      container.children.add(container.replace.children)
-      for child in container.children:
-        child.parent = container
-      container.replace.children.setLen(0)
-      if container.replace.parent != nil:
-        container.parent = container.replace.parent
-        let n = container.replace.parent.children.find(container.replace)
-        assert n != -1, "Container not a child of its parent"
-        container.parent.children[n] = container
-        container.replace.parent = nil
-      if pager.container == container.replace:
-        pager.setContainer(container)
-      pager.deleteContainer(container.replace)
-      container.replace = nil
-  of LOADED:
+      pager.replace(container)
+  else:
     dec pager.numload
-  of NEEDS_AUTH:
-    if pager.container == container:
-      pager.authorize()
-  of REDIRECT:
-    if container.redirectdepth < pager.config.network.max_redirect:
-      let url = event.request.url
-      if container.location.scheme == url.scheme or
-          container.location.scheme == "cgi-bin" or
-          container.location.scheme == "http" and url.scheme == "https" or
-          container.location.scheme == "https" and url.scheme == "http":
-        pager.redirectTo(container, event.request)
-      else:
-        pager.ask("Warning: switch protocols? " & $url).then(proc(x: bool) =
-          if x:
-            pager.redirectTo(container, event.request)
-        )
-    else:
-      pager.alert("Error: maximum redirection depth reached")
-      pager.deleteContainer(container)
-      return false
-  of ANCHOR:
-    let url2 = newURL(container.location)
+    pager.deleteContainer(container)
+    pager.redraw = true
+    pager.refreshStatusMsg()
+
+proc handleEvent0(pager: Pager; container: Container; event: ContainerEvent):
+    bool =
+  case event.t
+  of cetLoaded:
+    dec pager.numload
+  of cetAnchor:
+    let url2 = newURL(container.url)
     url2.setHash(event.anchor)
     pager.dupeBuffer(container, url2)
-  of NO_ANCHOR:
+  of cetNoAnchor:
     pager.alert("Couldn't find anchor " & event.anchor)
-  of UPDATE:
+  of cetUpdate:
     if container == pager.container:
       pager.redraw = true
-      if event.force: pager.term.clearCanvas()
-  of READ_LINE:
+      if event.force:
+        pager.term.clearCanvas()
+  of cetReadLine:
     if container == pager.container:
       pager.setLineEdit("(BUFFER) " & event.prompt, BUFFER, event.value, hide = event.password)
-  of READ_AREA:
+  of cetReadArea:
     if container == pager.container:
       var s = event.tvalue
       if openInEditor(pager.term, pager.config, pager.tmpdir, s):
@@ -1320,44 +1448,30 @@ proc handleEvent0(pager: Pager, container: Container, event: ContainerEvent): bo
       else:
         pager.container.readCanceled()
       pager.redraw = true
-  of OPEN:
+  of cetOpen:
     let url = event.request.url
     if pager.container == nil or not pager.container.isHoverURL(url):
       pager.ask("Open pop-up? " & $url).then(proc(x: bool) =
         if x:
-          pager.gotoURL(event.request, some(container.location),
+          pager.gotoURL(event.request, some(container.url),
             referrer = pager.container)
       )
     else:
-      pager.gotoURL(event.request, some(container.location),
+      pager.gotoURL(event.request, some(container.url),
         referrer = pager.container)
-  of INVALID_COMMAND: discard
-  of STATUS:
+  of cetStatus:
     if pager.container == container:
       pager.refreshStatusMsg()
-  of TITLE:
+  of cetSetLoadInfo:
+    if pager.container == container:
+      pager.onSetLoadInfo(container)
+  of cetTitle:
     if pager.container == container:
       pager.showAlerts()
       pager.term.setTitle(container.getTitle())
-  of ALERT:
+  of cetAlert:
     if pager.container == container:
       pager.alert(event.msg)
-  of CHECK_MAILCAP:
-    var (cm, connect) = pager.checkMailcap(container)
-    if cm == nil:
-      cm = container.connect2()
-    if connect:
-      cm.then(proc() =
-        container.startload())
-    else:
-      # remove "connecting..." message left by connect2
-      pager.refreshStatusMsg()
-      cm.then(proc(): auto =
-        container.quit())
-  of QUIT:
-    dec pager.numload
-    pager.deleteContainer(container)
-    return false
   return true
 
 proc handleEvents*(pager: Pager, container: Container) =