1

Kemal caching responses with handler middleware

I'm trying to do caching of some GET request with Kemal.

class CachingHandler < Kemal::Handler

    property cache : Hash(String, IO::Memory)

    def initialize
        @cache = Hash(String, IO::Memory).new
    end

    def call(context)
        puts "Caching"
        puts "Key: #{ context.request.resource }"
        if output = @cache[context.request.resource]?
            puts "Cache: true"
            IO.copy output, context.response.output
        else
            puts "Cache: false"
            puts "Cache: building"
            context.response.output          =
            @cache[context.request.resource] = IO::Memory.new
            # continue
            puts "Cache: continue"
            call_next context
        end
    end
end

But in the first request the browser it's always waiting a response. And in the second request send a "Closed stream (IO::Error)" error.

Javier Valencia
  • 697
  • 7
  • 24
  • Output IO [is closed](https://github.com/crystal-lang/crystal/blob/e2a1389e8165fb097c785524337d7bb7b550a086/src/http/server/request_processor.cr#L53) after each request. Next time you are trying to read from that IO, you get "Closed stream (IO::Error)" – Vitalii Elenhaupt Nov 05 '17 at 14:24

1 Answers1

2

You're indirectly setting context.response.output to IO::Memory.new. So, the next handler won't write to the connection's output stream but to the memory IO.

You'll need to copy the stream data to both memory and socket. Maybe a IO::MultiWriter can help with that, like response.output = IO::MultiWriter.new(response.output, memory_io).

Additionally, I'd recommend not to store IO::Memory instances but rather their raw data as Bytes (io.to_slice). Once you put it in the cache, there's no point in having an IO anymore. You can just write bytes directly to the output stream when the cache is hit (response.write(bytes).

Johannes Müller
  • 5,581
  • 1
  • 11
  • 25