0

In Http4s 0.16.6a, I had the following service.

import org.http4s.server.staticcontent._
import org.http4s._

object StaticFiles {

  val basepath = ...

  def apply(): HttpService = Service.lift(request => {
    val file = basepath + request.uri.path
    StaticFile.fromString(file, Some(request)).fold {
      Pass.now  // aka fallthrough
    } {
      NoopCacheStrategy.cache(request.pathInfo, _)
    }
  })
}

It takes the path from the url and tries to work out if a static file can be served. So a GET request to /index.html would try and load it with fromFile and if it can't be found, fallthrough or "Pass". When composed with other services using ||, it meant the total function (from lift) would be treated a bit like a partial function (from apply).

I can't seem to convert this over to Http4s 0.18.x.

The Http4s docs suggest the following:

import cats.effect._
import org.http4s._
import org.http4s.dsl.io._

import java.io.File

val service = HttpService[IO] {
  case request @ GET -> Root / "index.html" =>
    StaticFile.fromFile(new File("relative/path/to/index.html"), Some(request))
      .getOrElseF(NotFound()) // In case the file doesn't exist
}

Which is the basic form of what I'm trying to do, only I'd like to generify it a little and not create a partial function for every file I want to serve. i.e. avoid this:

case request @ GET -> Root / "index.html" => ???
case request @ GET -> Root / "fileA.html" => ???
case request @ GET -> Root / "fileB.html" => ???

So, my questions are:

  1. Is there the concept of Pass and passthough when using lift in 0.18?
  2. How to I use the NooopCacheStretegy with lift?
  3. Ultimately, how to I convert the code above to 0.18?

My endeavours so far have lead to this abomination (which obvs doesn't compile):

def apply(): HttpService[IO] = HttpService.lift(request => {
  val target = basepath + request.uri.path
  StaticFile.fromString[IO](target, Some(request)).fold {
    // passthrough
    IO.pure(???)
  } {
    // process request
    response => NoopCacheStrategy[IO].cache(request.pathInfo, response).unsafeRunSync()
  }
})

Note that I'm trying to use HttpService.lift not OptionT.liftF (as recommended). Mostly because I have no clue how to!

Toby
  • 9,523
  • 8
  • 36
  • 59

2 Answers2

1

So as far as I can tell, the concept of Pass has been replaced by OptionT in 0.18.x, with None playing the role of Pass. However, you don't have access to the OptionT with that overload. Instead, the assumption is that since you're passing a partial function to HttpService, the requests the function is defined on are precisely the ones you want this service to provide a response for.

You could try making it work with OptionT.lift, but I wouldn't recommend it either! Instead, I'd make a partial function that's only defined on arguments when your static files exists. The way http4s allows you to define the criteria needed to hit an endpoint through pattern-matching on requests is extremely powerful, and you're completely ignoring that option in both of your solutions.

As far as NoopCacheStrategy is concerned, I'm guessing the problem you ran into was that the return type of StaticFile.fromX is now IO[Response[IO]] and NoopCacheStrategy takes a Response[IO]. This is easily handled via flatMap.

Which is to say that this is what I came up with to translate your code to 0.18.x:

import java.nio.file.{Files, Paths}

import cats.effect.IO
import org.http4s.{HttpService, StaticFile}
import org.http4s.dsl.io._
import org.http4s.server.staticcontent.NoopCacheStrategy

  val service = HttpService[IO] {
    case request @ _ -> _ / file if Files.exists(Paths.get(file)) =>
      StaticFile
        .fromString(file, Some(request))
        .getOrElseF(NotFound())
        .flatMap(resp => NoopCacheStrategy[IO].cache(request.pathInfo, resp))
  }

A slight annoyance is that we're actually handling the case where no such file exists twice, once in the if clause of the case statement and once with getOrElseF. In practice, that NotFound should never be reached. I figure one can live with this.

And as an example of what I mean by the power of http4s' pattern matching on requests, by adjusting the case statement it's very easy to make sure this will only...

  • match on GET requests: case request @ GET -> _ / file if ...
  • match top-level files, nothing in subdirectories: case request @ _ -> Root / file if ...
  • match HTML files: case request @ _ -> _ / file ~ html if Files.exists(Paths.get(s"$file.html"))

You can even write your own custom extractor that checks whether you can serve a given file name to end up with something like case request @ _ -> _ / ValidStaticFile(file). That way you don't have to cram all your logic into the case statement.

Astrid
  • 1,808
  • 12
  • 24
0

I can't seem to format as a comment so posted as a new answer...

How about this?

  def apply(): HttpService[IO] = Kleisli.apply(request => {
    val basepath = ...
    val target = location + request.uri.path

    StaticFile.fromString[IO](target, Some(request))
  })
Toby
  • 9,523
  • 8
  • 36
  • 59