sourceafButter::ButterResponse.fan

using web::WebRes
using web::WebUtil
using util::JsonInStream

** The HTTP response.
class ButterResponse {

    ** The HTTP status code.
    Int statusCode

    ** The HTTP status message.
    Str statusMsg

    ** The HTTP repsonse headers.
    HttpResponseHeaders headers { private set }

    ** HTTP version of the response.
    Version version := Butter.http11

    ** The request body.
    Body    body { private set }

    ** A temporary store for request data, use to pass data between middleware.
    Str:Obj data    := [:]

    ** Creates a response reading real HTTP values from an 'InStream'.
    ** Note the whole response body is read in.
    new makeFromInStream(InStream in) {
//      // for testing
//      // note this WILL hang when reading a chunked response
//      out := `test/response2.txt`.toFile.out(false, 0)
//      while (true)
//          out.write(in.read).flush

        res := null as Str
        try {
            resVer  := (Version?) null
            res     = in.readLine
            if      (res == null) throw IOErr("Received no response")
            if      (res.startsWith("HTTP/1.0")) resVer = Butter.http10
            else if (res.startsWith("HTTP/1.1")) resVer = Butter.http11
            else throw IOErr("Unknown HTTP version: ${res}")

            statusCode  = res[9..11].toInt
            statusMsg   = res.size > 13 ? res[13..-1] : ""
            headers     = HttpResponseHeaders(WebUtil.parseHeaders(in))

            // See "Custom Charset Implementations" - https://fantom.org/forum/topic/2740
            // Should we perhaps check and replace the Charset with a fake valid one?
            // sys::ParseErr: Invalid Charset: 'ISO-8859': java.nio.charset.UnsupportedCharsetException: ISO-8859
            // sys::ParseErr: Invalid Charset: 'UTF-7': java.nio.charset.UnsupportedCharsetException: UTF-7
            //   fan.sys.Charset.fromStr (Charset.java:47)
            //   fan.sys.Charset.fromStr (Charset.java:26)
            //   fan.sys.MimeType.charset (MimeType.java:288)
            //   web::WebUtil.headersToCharset (WebUtil.fan:240)
            //   web::WebUtil.doMakeContentInStream (WebUtil.fan:281)
            //   web::WebUtil.makeContentInStream (WebUtil.fan:260)

            // ChunkInStream throws NullErr if the response has no body, e.g. HEAD requests
            // see http://fantom.org/sidewalk/topic/2365
            // I could check the Content-Length header, but why should I trust it!?
            instream := WebUtil.makeContentInStream(headers.val, in)

            body = Body(headers, instream)
        }
        catch (IOErr e) throw e
        catch (Err err) throw IOErr("Invalid HTTP response: $res", err)
    }

    ** Create a response. 'body' may either be a 'Str' or a 'Buf'.
    **
    ** This is a convenience ctor suitable for most applications.
    new make(Int statusCode, [Str:Str]? headers := null, Obj? body := null) {
        if (body != null && body isnot Str && body isnot Buf)
            throw ArgErr("Invalid Body, must be either null, Str, or Buf")
        this.statusCode = statusCode
        this.statusMsg  = WebRes.statusMsg[statusCode] ?: "Unknown"
        this.headers    = HttpResponseHeaders(headers)

        // can't use "switch" 'cos Buf is actually a MemBuf!
        if (body is Str)
            this.body   = Body(this.headers, (Str) body)
        else
        if (body is Buf)
            this.body   = Body(this.headers, (Buf) body)
        else
            this.body   = Body(this.headers, null as Str)
    }

    // Used by Bounce - so keep around!
    ** Create a response. 'body' may either be a 'Str' or a 'Buf'.
    new makeWithHeaders(Int statusCode, HttpResponseHeaders headers, Obj? body := null) {
        if (body != null && body isnot Str && body isnot Buf)
            throw ArgErr("Invalid Body, must be either null, Str, or Buf")
        this.statusCode = statusCode
        this.statusMsg  = WebRes.statusMsg[statusCode] ?: "Unknown"
        this.headers    = headers
        // can't use "switch" 'cos Buf is actually a MemBuf!
        if (body is Str)
            this.body   = Body(this.headers, (Str) body)
        else
        if (body is Buf)
            this.body   = Body(this.headers, (Buf) body)
        else
            this.body   = Body(this.headers, null as Str)
    }

    ** Dumps a debug string that in some way resembles the full HTTP response.
    Str dump(Bool dumpBody := true) {
        buf := StrBuf()
        out := buf.out

        out.print("HTTP/${version} ${statusCode} ${statusMsg}\n")
        headers.each |v, k| { out.print("${k}: ${v}\n") }
        out.print("\n")

        if (dumpBody)
            if (body.buf != null && body.buf.size > 0) {
                try   out.print(GzipMiddleware.deGzipResponse(this).readAllStr)
                catch out.print("** ERROR: Body does not contain string content **")
            }

        return buf.toStr
    }

    @NoDoc @Deprecated { msg="Use 'body.str' instead" }
    Str? asStr() {
        body.str
    }

    @NoDoc @Deprecated { msg="Use 'body.buf' instead" }
    Buf? asBuf() {
        body.buf
    }

    @NoDoc @Deprecated { msg="Use 'body.buf.seek(0).in' instead" }
    InStream? asInStream() {
        body.buf?.seek(0)?.in
    }

    @NoDoc @Deprecated { msg="Use 'body.jsonObj' instead" }
    Obj? asJson() {
        body.jsonObj
    }

    @NoDoc @Deprecated { msg="Use 'body.jsonMap' instead" }
    [Str:Obj?]? asJsonMap() {
        body.jsonMap
    }

    @NoDoc
    override Str toStr() {
        "$statusCode - $statusMsg"
    }
}