require "spec" require "http/server/response" require "http/headers" require "http/status" require "http/cookie" private alias Response = HTTP::Server::Response private class ReverseResponseOutput < IO @output : IO def initialize(@output : IO) end def write(slice : Bytes) : Nil slice.reverse_each do |byte| @output.write_byte(byte) end end def read(slice : Bytes) raise "Not implemented" end def close @output.close end def flush @output.flush end end describe HTTP::Server::Response do it "closes" do io = IO::Memory.new response = Response.new(io) response.close response.closed?.should be_true io.closed?.should be_false expect_raises(IO::Error, "Closed stream") { response << "foo" } io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 0\r\n\r\n") end it "does not automatically add the `content-length` header if the response is a 304" do io = IO::Memory.new response = Response.new(io) response.status = :not_modified response.close io.to_s.should eq("HTTP/1.1 304 Not Modified\r\n\r\n") end it "does not automatically add the `content-length` header if the response is a 204" do io = IO::Memory.new response = Response.new(io) response.status = :no_content response.close io.to_s.should eq("HTTP/1.1 204 No Content\r\n\r\n") end it "does not automatically add the `content-length` header if the response is informational" do io = IO::Memory.new response = Response.new(io) response.status = :processing response.close io.to_s.should eq("HTTP/1.1 102 Processing\r\n\r\n") end # Case where the content-length represents the size of the data that would have been returned. it "allows specifying the content-length header explicitly" do io = IO::Memory.new response = Response.new(io) response.status = :not_modified response.headers["Content-Length"] = "5" response.close io.to_s.should eq("HTTP/1.1 304 Not Modified\r\nContent-Length: 5\r\n\r\n") end it "allow explicitly configuring a `Transfer-Encoding` response" do io = IO::Memory.new response = Response.new(io) response.headers["Transfer-Encoding"] = "chunked" response.print "Hello" response.close io.to_s.should eq("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHello\r\n0\r\n\r\n") end it "prints less then buffer's size" do io = IO::Memory.new response = Response.new(io) response.print("Hello") response.close io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nHello") end it "prints less then buffer's size to output" do io = IO::Memory.new response = Response.new(io) response.output.print("Hello") response.output.close io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nHello") end it "prints more then buffer's size" do io = IO::Memory.new response = Response.new(io) str = "1234567890" slices = (IO::DEFAULT_BUFFER_SIZE // 10) slices.times do response.print(str) end response.print(str) response.close first_chunk = str * slices second_chunk = str io.to_s.should eq("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n#{(first_chunk.bytesize).to_s(16)}\r\n#{first_chunk}\r\n#{(second_chunk.bytesize).to_s(16)}\r\n#{second_chunk}\r\n0\r\n\r\n") end it "prints with content length" do io = IO::Memory.new response = Response.new(io) response.headers["Content-Length"] = "10" response.print("1234") response.print("567890") response.close io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\n1234567890") end it "prints with content length (method)" do io = IO::Memory.new response = Response.new(io) response.content_length = 10 response.print("1234") response.print("567890") response.close io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\n1234567890") end it "doesn't override content-length when there's no body" do io = IO::Memory.new response = Response.new(io) response.content_length = 10 response.close io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 10\r\n\r\n") end it "adds header" do io = IO::Memory.new response = Response.new(io) response.headers["Content-Type"] = "text/plain" response.print("Hello") response.close io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 5\r\n\r\nHello") end it "sets content type" do io = IO::Memory.new response = Response.new(io) response.content_type = "text/plain" response.print("Hello") response.close io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Type: text/plain\r\nContent-Length: 5\r\n\r\nHello") end it "gets content type" do io = IO::Memory.new response = Response.new(io) response.content_type.should be_nil response.content_type = "text/plain" response.content_type.should eq "text/plain" end it "gets content length" do io = IO::Memory.new response = Response.new(io) response.content_length.should be_nil response.content_length = 10 response.content_length.should eq 10 end it "sets content type after headers sent" do io = IO::Memory.new response = Response.new(io) response.print("Hello") response.flush expect_raises(IO::Error, "Headers already sent") do response.content_type = "text/plain" end end it "sets status code" do io = IO::Memory.new response = Response.new(io) return_value = response.status_code = 201 return_value.should eq 201 response.status.should eq HTTP::Status::CREATED response.status_message.should eq "Created" response.print("Hello") response.flush expect_raises(IO::Error, "Headers already sent") do response.status_code = 201 end end it "retrieves status code" do io = IO::Memory.new response = Response.new(io) response.status = :created response.status_code.should eq 201 end it "changes status message" do io = IO::Memory.new response = Response.new(io) response.status = :not_found response.status_message = "Custom status" response.close io.to_s.should eq("HTTP/1.1 404 Custom status\r\nContent-Length: 0\r\n\r\n") response.status_message.should eq "Custom status" expect_raises(IO::Error, "Closed stream") do response.status_message = "Other status" end end it "changes status and others" do io = IO::Memory.new response = Response.new(io) response.status = :not_found response.version = "HTTP/1.0" response.close io.to_s.should eq("HTTP/1.0 404 Not Found\r\nContent-Length: 0\r\n\r\n") end it "changes status and others after headers sent" do io = IO::Memory.new response = Response.new(io) response.print("Foo") response.flush expect_raises(IO::Error, "Headers already sent") do response.status = :not_found end expect_raises(IO::Error, "Headers already sent") do response.version = "HTTP/1.0" end end it "closes gracefully with replaced output that syncs close (#11389)" do output = IO::Memory.new response = HTTP::Server::Response.new(output) response.output = IO::Stapled.new(response.output, response.output, sync_close: true) response.print "some body" response.close output.rewind.gets_to_end.should eq "HTTP/1.1 200 OK\r\nContent-Length: 9\r\n\r\nsome body" end it "flushes" do io = IO::Memory.new response = Response.new(io) response.print("Hello") response.flush io.to_s.should eq("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHello\r\n") response.close io.to_s.should eq("HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nHello\r\n0\r\n\r\n") end it "wraps output" do io = IO::Memory.new response = Response.new(io) response.output = ReverseResponseOutput.new(response.output) response.print("1234") response.close io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 4\r\n\r\n4321") end it "writes and flushes with HTTP 1.0" do io = IO::Memory.new response = Response.new(io, "HTTP/1.0") response.print("1234") response.flush io.to_s.should eq("HTTP/1.0 200 OK\r\n\r\n1234") end it "resets and clears headers and cookies" do io = IO::Memory.new response = Response.new(io) response.headers["Foo"] = "Bar" response.cookies["Bar"] = "Foo" response.status = HTTP::Status::USE_PROXY response.status_message = "Baz" response.reset response.headers.should be_empty response.cookies.should be_empty response.status.should eq HTTP::Status::OK response.status_message.should eq "OK" end it "writes cookie headers" do io = IO::Memory.new response = Response.new(io) response.cookies["Bar"] = "Foo" response.close io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 0\r\nSet-Cookie: Bar=Foo\r\n\r\n") io = IO::Memory.new response = Response.new(io) response.cookies["Bar"] = "Foo" response.print("Hello") response.close io.to_s.should eq("HTTP/1.1 200 OK\r\nContent-Length: 5\r\nSet-Cookie: Bar=Foo\r\n\r\nHello") end it "closes when it fails to write" do io = IO::Memory.new response = Response.new(io) response.print("Hello") response.flush io.close response.print("Hello") expect_raises(HTTP::Server::ClientError) { response.flush } response.closed?.should be_true end describe "#respond_with_status" do it "uses default values" do io = IO::Memory.new response = Response.new(io) response.content_type = "text/html" response.respond_with_status(500) io.to_s.should eq("HTTP/1.1 500 Internal Server Error\r\nContent-Type: text/plain\r\nContent-Length: 26\r\n\r\n500 Internal Server Error\n") response.status_message.should eq "Internal Server Error" end it "sends custom code and message" do io = IO::Memory.new response = Response.new(io) response.respond_with_status(400, "Request Error") io.to_s.should eq("HTTP/1.1 400 Request Error\r\nContent-Type: text/plain\r\nContent-Length: 18\r\n\r\n400 Request Error\n") response.status_message.should eq "Request Error" end it "sends HTTP::Status" do io = IO::Memory.new response = Response.new(io) response.respond_with_status(HTTP::Status::URI_TOO_LONG) io.to_s.should eq("HTTP/1.1 414 URI Too Long\r\nContent-Type: text/plain\r\nContent-Length: 17\r\n\r\n414 URI Too Long\n") response.status_message.should eq "URI Too Long" end it "sends HTTP::Status and custom message" do io = IO::Memory.new response = Response.new(io) response.respond_with_status(HTTP::Status::URI_TOO_LONG, "Request Error") io.to_s.should eq("HTTP/1.1 414 Request Error\r\nContent-Type: text/plain\r\nContent-Length: 18\r\n\r\n414 Request Error\n") response.status_message.should eq "Request Error" end it "raises when response is closed" do io = IO::Memory.new response = Response.new(io) response.close expect_raises(IO::Error, "Closed stream") do response.respond_with_status(400) end end it "raises when headers written" do io = IO::Memory.new response = Response.new(io) response.print("Hello") response.flush expect_raises(IO::Error, "Headers already sent") do response.respond_with_status(400) end end end describe "#redirect" do ["/path", URI.parse("/path")].each do |location| it "#{location.class} location" do io = IO::Memory.new response = Response.new(io) response.redirect(location) io.to_s.should eq("HTTP/1.1 302 Found\r\nLocation: /path\r\nContent-Length: 0\r\n\r\n") end end it "encodes special characters" do io = IO::Memory.new response = Response.new(io) response.redirect("https://example.com/path\nfoo bar") io.to_s.should eq("HTTP/1.1 302 Found\r\nLocation: https://example.com/path%0Afoo%20bar\r\nContent-Length: 0\r\n\r\n") end it "doesn't encode URIs twice" do io = IO::Memory.new response = Response.new(io) u = URI.new "https", host: "example.com", path: "auth", query: URI::Params.new({"redirect_uri" => ["http://example.com/callback"]}) response.redirect(u) io.to_s.should eq("HTTP/1.1 302 Found\r\nLocation: https://example.com/auth?redirect_uri=http%3A%2F%2Fexample.com%2Fcallback\r\nContent-Length: 0\r\n\r\n") end it "permanent redirect" do io = IO::Memory.new response = Response.new(io) response.redirect("/path", status: :moved_permanently) io.to_s.should eq("HTTP/1.1 301 Moved Permanently\r\nLocation: /path\r\nContent-Length: 0\r\n\r\n") end it "with header" do io = IO::Memory.new response = Response.new(io) response.headers["Foo"] = "Bar" response.redirect("/path", status: :moved_permanently) io.to_s.should eq("HTTP/1.1 301 Moved Permanently\r\nFoo: Bar\r\nLocation: /path\r\nContent-Length: 0\r\n\r\n") end it "fails if headers already sent" do io = IO::Memory.new response = Response.new(io) response.puts "foo" response.flush expect_raises(IO::Error, "Headers already sent") do response.redirect("/path") end end it "fails if closed" do io = IO::Memory.new response = Response.new(io) response.close expect_raises(IO::Error, "Closed stream") do response.redirect("/path") end end end end