Merge: Changed nitcorn HTTPResponse body type to Writable
authorJean Privat <jean@pryen.org>
Fri, 1 Jun 2018 17:10:06 +0000 (13:10 -0400)
committerJean Privat <jean@pryen.org>
Fri, 1 Jun 2018 17:10:06 +0000 (13:10 -0400)
This PR allows to send binaries (and whatever writable document) as bodies in nitcorn's responses.

The reduced use of buffers and copies should also reduce the pressure on the GC and make things faster.

Embrace, extend and extinguish #2703

Pull-Request: #2704
Reviewed-by: Lucas Bajolet <r4pass@hotmail.com>

contrib/benitlux/src/server/benitlux_controller.nit
contrib/nitiwiki/src/wiki_edit.nit
contrib/nitrpg/src/web.nit
contrib/opportunity/src/opportunity_controller.nit
lib/core/stream.nit
lib/nitcorn/file_server.nit
lib/nitcorn/http_response.nit
lib/nitcorn/reactor.nit
lib/popcorn/pop_handlers.nit
tests/sav/test_nitcorn.res
tests/test_nitcorn.nit

index a2ab392..8f96182 100644 (file)
@@ -77,7 +77,7 @@ class BenitluxSubscriptionAction
                end
 
                var response = new HttpResponse(200)
-               response.body = template.write_to_string
+               response.body = template
                return response
        end
 end
index 3b1abb7..436760b 100644 (file)
@@ -50,7 +50,7 @@ class WikiEditForm
        fun to_http_response: HttpResponse
        do
                var resp = new HttpResponse(200)
-               resp.body = tpl_page.write_to_string
+               resp.body = tpl_page
                return resp
        end
 end
index f624f48..c563af7 100644 (file)
@@ -50,7 +50,7 @@ class RpgAction
                var page = new NitRpgPage(root_url)
                var error = new ErrorPanel(msg)
                page.flow_panels.add error
-               rsp.body = page.write_to_string
+               rsp.body = page
                return rsp
        end
 
@@ -95,7 +95,7 @@ class RpgHome
                page = new NitRpgPage(root_url)
                page.side_panels.add new GamesShortListPanel(root_url, games)
                page.flow_panels.add new MDPanel(readme)
-               response.body = page.write_to_string
+               response.body = page
                return response
        end
 
@@ -126,7 +126,7 @@ class ListGames
                page.breadcrumbs = new Breadcrumbs
                page.breadcrumbs.add_link(root_url / "games", "games")
                page.flow_panels.add new GamesListPanel(root_url, games)
-               response.body = page.write_to_string
+               response.body = page
                return response
        end
 end
@@ -196,7 +196,7 @@ class RepoHome
                page.flow_panels.add new PodiumPanel(game)
                page.flow_panels.add new EventListPanel(game, list_limit, list_from)
                page.flow_panels.add new AchievementsListPanel(game)
-               rsp.body = page.write_to_string
+               rsp.body = page
                return rsp
        end
 end
@@ -210,7 +210,7 @@ class ListPlayers
                if is_response_error(rsp) then return rsp
                page.breadcrumbs.add_link(game.url / "players", "players")
                page.flow_panels.add new ListPlayersPanel(game)
-               rsp.body = page.write_to_string
+               rsp.body = page
                return rsp
        end
 end
@@ -239,7 +239,7 @@ class PlayerHome
                page.flow_panels.add new PlayerWorkPanel(game, player)
                page.flow_panels.add new AchievementsListPanel(player)
                page.flow_panels.add new EventListPanel(player, list_limit, list_from)
-               rsp.body = page.write_to_string
+               rsp.body = page
                return rsp
        end
 end
@@ -253,7 +253,7 @@ class ListAchievements
                if is_response_error(rsp) then return rsp
                page.breadcrumbs.add_link(game.url / "achievements", "achievements")
                page.flow_panels.add new AchievementsListPanel(game)
-               rsp.body = page.write_to_string
+               rsp.body = page
                return rsp
        end
 end
@@ -278,7 +278,7 @@ class AchievementHome
                page.breadcrumbs.add_link(achievement.url, achievement.name)
                page.flow_panels.add new AchievementPanel(achievement)
                page.flow_panels.add new EventListPanel(achievement, list_limit, list_from)
-               rsp.body = page.write_to_string
+               rsp.body = page
                return rsp
        end
 end
index 214c843..3266502 100644 (file)
@@ -31,7 +31,7 @@ abstract class OpportunityAction
        # TODO: Add a specific body to the bad request page.
        fun bad_req: HttpResponse do
                var rsp = new HttpResponse(400)
-               rsp.body = (new OpportunityHomePage).write_to_string
+               rsp.body = new OpportunityHomePage
                return rsp
        end
 end
@@ -73,7 +73,7 @@ class OpportunityWelcome
                                meetpage.ans = ansset
                                meetpage.meet = meet
                                meetpage.error = "'Meetup name' is a mandatory fields."
-                               rsp.body = meetpage.write_to_string
+                               rsp.body = meetpage
                                return rsp
 
                        end
@@ -85,7 +85,7 @@ class OpportunityWelcome
                                var meetpage = new MeetupCreationPage
                                meetpage.meet = meet
                                meetpage.error = "You need to input at least one answer."
-                               rsp.body = meetpage.write_to_string
+                               rsp.body = meetpage
                                return rsp
                        end
 
@@ -100,7 +100,7 @@ class OpportunityWelcome
 <p>Failed to create event</p>
 <p>This is a server side error, it has been logged.
    You may still want to contact the maintainers of this website.</p>"""
-                               rsp.body = meetpage.write_to_string
+                               rsp.body = meetpage
                                return rsp
                        end
 
@@ -111,22 +111,22 @@ class OpportunityWelcome
                        end
                        db.close
                        var rsp = new HttpResponse(200)
-                       rsp.body = (new MeetupConfirmation(meet)).write_to_string
+                       rsp.body = new MeetupConfirmation(meet)
                        return rsp
                end
                if rq.has("new_meetup") then
                        var rsp = new HttpResponse(200)
                        var page = new MeetupCreationPage
-                       rsp.body = page.write_to_string
+                       rsp.body = page
                        return rsp
                end
                if get.has_key("meetup_id") then
                        var rsp = new HttpResponse(200)
-                       rsp.body = (new OpportunityMeetupPage.from_id(get["meetup_id"])).write_to_string
+                       rsp.body = new OpportunityMeetupPage.from_id(get["meetup_id"])
                        return rsp
                end
                var rsp = new HttpResponse(200)
-               rsp.body = (new OpportunityHomePage).write_to_string
+               rsp.body = new OpportunityHomePage
                return rsp
        end
 
index 3fc3f30..6d0278a 100644 (file)
@@ -574,17 +574,32 @@ interface Writable
        # The specific logic it let to the concrete subclasses
        fun write_to(stream: Writer) is abstract
 
-       # Like `write_to` but return a new String (may be quite large)
+       # Like `write_to` but return a new String (may be quite large).
        #
-       # This funtionality is anectodical, since the point
-       # of streamable object to to be efficienlty written to a
-       # stream without having to allocate and concatenate strings
+       # This functionality is anecdotal, since the point
+       # of a streamable object is to be efficiently written to a
+       # stream without having to allocate and concatenate strings.
        fun write_to_string: String
        do
                var stream = new StringWriter
                write_to(stream)
                return stream.to_s
        end
+
+       # Like `write_to` but return a new Bytes (may be quite large)
+       #
+       # This functionality is anecdotal, since the point
+       # of a streamable object is to be efficiently written to a
+       # stream without having to allocate and concatenate buffers.
+       #
+       # Nevertheless, you might need this method if you want to know
+       # the byte size of a writable object.
+       fun write_to_bytes: Bytes
+       do
+               var stream = new BytesWriter
+               write_to(stream)
+               return stream.bytes
+       end
 end
 
 redef class Bytes
index a9ef5f6..96293f6 100644 (file)
@@ -125,7 +125,7 @@ class FileServer
                if response.status_code != 200 then
                        var tmpl = error_page(response.status_code)
                        if header != null and tmpl isa ErrorTemplate then tmpl.header = header
-                       response.body = tmpl.to_s
+                       response.body = tmpl
                end
 
                return response
index ec83289..97276ca 100644 (file)
@@ -20,6 +20,7 @@
 module http_response
 
 import serialization
+private import template
 
 # A response to send over HTTP
 class HttpResponse
@@ -38,7 +39,7 @@ class HttpResponse
        var header = new HashMap[String, String]
 
        # Body of this response
-       var body = "" is writable
+       var body: Writable = "" is writable
 
        # Files appended after `body`
        var files = new Array[String]
@@ -49,7 +50,20 @@ class HttpResponse
                # Set the content length if not already set
                if not header.keys.has("Content-Length") then
                        # Size of the body
-                       var len = body.byte_length
+                       var len
+                       var body = self.body
+                       if body isa Text then
+                               len = body.byte_length
+                       else if body isa Bytes then
+                               len = body.length
+                       else
+                               # We need the length, but there is no length in a writable.
+                               # So just render it as a bytes then measure :/
+                               body = body.write_to_bytes
+                               len = body.length
+                               # Keep the body as bytes since we have it
+                               self.body = body
+                       end
 
                        # Size of included files
                        for path in files do
@@ -77,17 +91,18 @@ class HttpResponse
        end
 
        # Get this reponse as a string according to HTTP protocol
-       redef fun to_s
+       fun render: Writable
        do
                finalize
 
-               var buf = new FlatBuffer
-               buf.append("{http_version} {status_code} {status_message or else ""}\r\n")
+               var buf = new Template
+               buf.add("{http_version} {status_code} {status_message or else ""}\r\n")
                for key, value in header do
-                       buf.append("{key}: {value}\r\n")
+                       buf.add("{key}: {value}\r\n")
                end
-               buf.append("\r\n{body}")
-               return buf.to_s
+               buf.add("\r\n")
+               buf.add body
+               return buf
        end
 end
 
index afa0d0a..85a2068 100644 (file)
@@ -91,7 +91,7 @@ class HttpServer
        # Send back `response` to the client
        fun respond(response: HttpResponse)
        do
-               write response.to_s
+               response.render.write_to(self)
                for path in response.files do write_file path
        end
 end
index e657d68..cd7a380 100644 (file)
@@ -431,7 +431,7 @@ redef class HttpResponse
        # Write data in body response and send it.
        fun send(raw_data: nullable Writable, status: nullable Int) do
                if raw_data != null then
-                       body += raw_data.write_to_string
+                       body = raw_data
                end
                if status != null then
                        status_code = status
index 9ee42f3..6a13937 100644 (file)
@@ -128,3 +128,10 @@ Content-Length: 0
 Server: nitcorn\r
 Set-Cookie: nitcorn_session=; HttpOnly; expires=Thu, 01 Jan 1970 00:00:00 GMT\r
 \r
+
+[Client] curl -s localhost:*****/simple_binary/
+ñòó
+
+[Client] curl -s localhost:*****/simple_template/
+Hello
+ñòó
index b7fcd90..7f9c9d6 100644 (file)
@@ -30,23 +30,49 @@ class MyAction
        redef fun answer(request, turi)
        do
                var rep = new HttpResponse(200)
-               rep.body = """
+               var body = """
 [Response] Simple answer
 Method: {{{request.method}}}, URI: {{{request.uri}}}, trailing: {{{turi}}}"""
 
                if request.get_args.not_empty
-               then rep.body += "\nGET args: {request.get_args.join(", ", ":")}"
+               then body += "\nGET args: {request.get_args.join(", ", ":")}"
 
                if request.post_args.not_empty
-               then rep.body += "\nPOST args: {request.post_args.join(", ", ":")}"
+               then body += "\nPOST args: {request.post_args.join(", ", ":")}"
 
                if request.uri_params.not_empty
-               then rep.body += "\nParams args: {request.uri_params.join(", ", ":")}"
+               then body += "\nParams args: {request.uri_params.join(", ", ":")}"
 
                if request.cookie.not_empty
-               then rep.body += "\nCookie: {request.cookie.join(", ", ":")}"
+               then body += "\nCookie: {request.cookie.join(", ", ":")}"
 
-               rep.body += "\n"
+               body += "\n"
+               rep.body = body
+               return rep
+       end
+end
+
+class MyBinAction
+       super Action
+
+       redef fun answer(request, turi)
+       do
+               var rep = new HttpResponse(200)
+               rep.body = b"\xF1\xF2\xF3\n"
+               return rep
+       end
+end
+
+class MyTmplAction
+       super Action
+
+       redef fun answer(request, turi)
+       do
+               var rep = new HttpResponse(200)
+               var body = new Template
+               body.add "Hello\n"
+               body.add b"\xF1\xF2\xF3\n"
+               rep.body = body
                return rep
        end
 end
@@ -64,6 +90,8 @@ class ServerThread
                vh.routes.add new Route("file_server", new FileServer(fs_path.simplify_path))
                vh.routes.add new Route("simple_answer", new MyAction)
                vh.routes.add new Route("params_answer/:i/:s", new MyAction)
+               vh.routes.add new Route("simple_binary", new MyBinAction)
+               vh.routes.add new Route("simple_template", new MyTmplAction)
 
                # Launch
                var factory = new HttpFactory.and_libevent
@@ -104,6 +132,9 @@ class ClientThread
                system "curl -s {iface}/file_server/unknown_file.txt --head"
 
                system "curl -s {iface}/invalid_route --head"
+
+               system "curl -s {iface}/simple_binary/"
+               system "curl -s {iface}/simple_template/"
                return null
        end