nodemcu / nodemcu-firmware

Lua based interactive firmware for ESP8266, ESP8285 and ESP32
https://nodemcu.readthedocs.io
MIT License
7.67k stars 3.13k forks source link

Lua httpserver duplicates status line #3616

Open athompson673 opened 1 year ago

athompson673 commented 1 year ago

Expected behavior

http status line is only sent once at the beginning of a response

Actual behavior

calling res:send(nil, status) then res:send(body_data, nil) results in duplicate status line being inserted at the end of the headers before the body. This caused me errors in certain http clients (particularly NodeRed).

Test code

Provide a Minimal, Complete, and Verifiable example which will reproduce the problem.

-- run http-example.lua

from a terminal call curl -v node_addr/:

> HTTP/1.1 200 OK
> Transfer-Encoding: chunked
> Connection: close
HTTP/1.1 200 OK* <- error on extra status line (missing colon (bad header line format))

Proposed solution

While this could be fixed in-place with a flag on whether the status has been already sent, I believe the most appropriate solution is to make a breaking change to httpserver.lua by separating the function to send the status line and body data. I don't see it as substantially more complicated to the user to replace the first call with res:send_status(200) and eliminate the status argument from res:send other than that it is a breaking change.

NodeMCU startup banner

recent build of release branch from cloud build service

Hardware

adafruit feather 8266

athompson673 commented 1 year ago

This would probably do the trick, though I haven't had a chance to test it yet:

    ------------------------------------------------------------------------------
    -- response methods
    ------------------------------------------------------------------------------
    local make_res = function(csend, cfini)
        local send_status = function(self, status, reason)
            csend("HTTP/1.1 ")
            if status then
                csend(" " .. tostring(status))
            else
                csend(" 200")
            end
            if reason then
                csend((" %s\r\n"):format(reason))
            else
                csend(" OK\r\n")
            end
            csend("Transfer-Encoding: chunked\r\n") --always use chunked transfer
            self.send_status = nil --only send status once
        end

        local send_header = function(self, name, value)
            if self.send_status then
                self:send_status()
            end
            csend(("%s: %s\r\n"):format(name, value))
        end

        local send = function(self, data)
            if self.send_status then
                self:send_status()
            end
            if self.send_header then
                csend("\r\n") --blank line indicates end of headers
                self.send_header = nil
            end
            -- chunked transfer encoding
            csend(("%X\r\n%s\r\n"):format(#data,data))
        end

        -- finalize request
        local finish = function(self)
            if self.send_status then
                self:send_status()
            end
            if self.send_header then
                csend("\r\n") --blank line indicates end of headers
                self.send_header = nil
            end
            -- finalize chunked transfer encoding
            csend("0\r\n\r\n")
            -- close connection
            cfini()
        end
        --
        local res = { }
        res.send_status = send_status
        res.send_header = send_header
        res.send = send
        res.finish = finish
        return res
    end

The example file would then be changed as such:

    -- reply
    res:send_status(200, "OK")
    res:send_header("Connection", "close")
    res:send("Hello, world!\n")
    res:finish()