Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add HttpMethodOverrider middleware #2355

Closed
@@ -0,0 +1,140 @@
package org.http4s
package server
package middleware

import cats.data.Kleisli
import cats.effect.Sync
import cats.implicits._
import cats.{Monad, ~>}
import org.http4s.Http
import org.http4s.util.CaseInsensitiveString

import scala.reflect.runtime.universe._

object HttpMethodOverrider {

/**
* HttpMethodOverrider middleware config options.
*/
class HttpMethodOverriderConfig(
val overrideStrategy: OverrideStrategy,
val overridableMethods: Set[Method]) {

type Self = HttpMethodOverriderConfig

private def copy(
overrideStrategy: OverrideStrategy = overrideStrategy,
overridableMethods: Set[Method] = overridableMethods
): Self =
new HttpMethodOverriderConfig(overrideStrategy, overridableMethods)

def withOverrideStrategy(overrideStrategy: OverrideStrategy): Self =
copy(overrideStrategy = overrideStrategy)

def withOverridableMethods(overridableMethods: Set[Method]): Self =
copy(overridableMethods = overridableMethods)
}

object HttpMethodOverriderConfig {
def apply(
overrideStrategy: OverrideStrategy,
overridableMethods: Set[Method]): HttpMethodOverriderConfig =
new HttpMethodOverriderConfig(overrideStrategy, overridableMethods)
}

sealed trait OverrideStrategy
final case class HeaderOverrideStrategy(headerName: CaseInsensitiveString)
extends OverrideStrategy
final case class QueryOverrideStrategy(paramName: String) extends OverrideStrategy
final case class FormOverrideStrategy[G[_], F[_]](
fieldName: String,
naturalTransformation: G ~> F)
This conversation was marked as resolved by nebtrx

This comment has been minimized.

Copy link
@rossabaker

rossabaker Jan 24, 2019

Member

I agree with limiting the ~> to this one. I bet it's the most obscure of them.

This comment has been minimized.

Copy link
@nebtrx

nebtrx Feb 1, 2019

Author Contributor

馃憤

extends OverrideStrategy

val defaultConfig = HttpMethodOverriderConfig(
HeaderOverrideStrategy(CaseInsensitiveString("X-HTTP-Method-Override")),
Set(Method.POST))

val overriddenMethodAttrKey: AttributeKey[Method] = AttributeKey[Method]

/** Simple middleware for HTTP Method Override.
*
* This middleware lets you use HTTP verbs such as PUT or DELETE in places where the client
* doesn't support it. Camouflage your request with another HTTP verb(usually POST) and sneak
* the desired one using a custom header or request parameter. The middleware will '''override'''
* the original verb with the new one for you, allowing the request the be dispatched properly.
*
* @param http [[Http]] to transform
* @param config http method overrider config
*/
def apply[F[_], G[_]](http: Http[F, G], config: HttpMethodOverriderConfig)(
implicit F: Monad[F],
S: Sync[G],
TT: TypeTag[G ~> F]): Http[F, G] = {

lazy val runtimeTypeNT = implicitly[TypeTag[G ~> F]].tpe

val parseMethod = (m: String) => Method.fromString(m.toUpperCase)

val processRequestWithOriginalMethod = (req: Request[G]) => http(req)

def processRequestWithMethod(
req: Request[G],
parseResult: ParseResult[Method]): F[Response[G]] = parseResult match {
case Left(_) => F.pure(Response[G](Status.BadRequest))
case Right(om) => http(updateRequestWithMethod(req, om)).map(updateVaryHeader)
}

def updateVaryHeader(resp: Response[G]): Response[G] = {
val varyHeaderName = CaseInsensitiveString("Vary")
config.overrideStrategy match {
case HeaderOverrideStrategy(headerName) =>
val updatedVaryHeader =
resp.headers
.get(varyHeaderName)
.map((h: Header) => Header(h.name.value, s"${h.value}, ${headerName.value}"))
This conversation was marked as resolved by rossabaker

This comment has been minimized.

Copy link
@rossabaker

rossabaker Jan 14, 2019

Member

Would be nice if we had a modeled Vary header which we could add to, but that could be a separate enhancement.

This comment has been minimized.

Copy link
@nebtrx

nebtrx Jan 15, 2019

Author Contributor

Agreed, perhaps you can open an issue explaining more details. I'll be glad to help in that one.

This comment has been minimized.

Copy link
@rossabaker

rossabaker Jan 24, 2019

Member

It's one of many mentioned in #2011.

.getOrElse(Header(varyHeaderName.value, headerName.value))

resp.withHeaders(resp.headers.put(updatedVaryHeader))
case _ => resp
}
}

def updateRequestWithMethod(req: Request[G], om: Method): Request[G] = {
val attrs = req.attributes ++ Seq(overriddenMethodAttrKey(req.method))
req.withAttributes(attrs).withMethod(om)
}

def getUnsafeOverrideMethod(req: Request[G]): F[Option[String]] =
config.overrideStrategy match {

This comment has been minimized.

Copy link
@rossabaker

rossabaker Jan 14, 2019

Member

Are there other plausible strategies, such that this might be a function on the strategy and unseal the strategy? People don't embed this in a UrlForm, do they?

This comment has been minimized.

Copy link
@nebtrx

nebtrx Jan 15, 2019

Author Contributor

Ohh, I wasn't aware of UrlForm. I have to take a look at it since I was only thinking about QueryParamDecoder. The Query Param Strategy might get in the middle. If so, We should find a way to update the request and remove the param once it has been overridden.

I'm also considering a strategy like what you say. Perhaps something that receives a Request[G] => Request[G] or Request[G] => Method. The first one seems to lose some control over the request pipeline and the second one...mmm....perhaps it exposes too much information to just override the Request method.

I'll think about this.

case HeaderOverrideStrategy(headerName) => F.pure(req.headers.get(headerName).map(_.value))
case QueryOverrideStrategy(parameter) => F.pure(req.params.get(parameter))
case FormOverrideStrategy(field, f) if runtimeTypeNT == typeOf[G ~> F] =>

This comment has been minimized.

Copy link
@rossabaker

rossabaker Jan 24, 2019

Member

This scares me. I'm too sleepy to think of how to do this without reflection. But I'm not sure reflection makes it much safer anyway: aren't you trading a ClassCastException for a MatchError?

This comment has been minimized.

Copy link
@nebtrx

nebtrx Feb 1, 2019

Author Contributor

Yeah, it scared me too :S and I hate reflection. I spend ~ two hours looking for the proper/elegant way of overcoming type erasure in this kind of scenarios and there seems to a general consensus about using the following approaches:

  1. Using ClassTag[_], TypeTag[_] or WeakTypeTag[_]
  2. Using Manifest[_]

Both are pretty similar. I had a slightly bigger preference for the first one and since Headerkey is already using that approach with ClassTag, I went for it.

Let me know if have a better approach. I keep looking for better ways to handle this

This comment has been minimized.

Copy link
@nebtrx

nebtrx Feb 1, 2019

Author Contributor

BTW, yes, I'd be trading ClassCastException for MatchError. It kind of feels more logical to me since we trying to match strategies. WDYT?

This comment has been minimized.

Copy link
@rossabaker

rossabaker Feb 3, 2019

Member

I can't think of a better solution, and I think it would also be difficult to use it incorrectly. I'm inclined to roll with it. I'd rather have the feature, and if someone can think of something more clever later, so much the better.

This comment has been minimized.

Copy link
@ChristopherDavenport

ChristopherDavenport Feb 4, 2019

Member

Hmm, I'm not sure if introducing blockers to the ScalaJS plan is fantastic.

val nt = f.asInstanceOf[G ~> F]
for {
formFields <- nt(
UrlForm
.entityDecoder[G]
.decode(req, strict = true)
.value
.map(_.toOption.map(_.values)))

} yield formFields.flatMap(_.get(field).flatMap(_.uncons.map(_._1)))
}

def processRequest(req: Request[G]): F[Response[G]] = getUnsafeOverrideMethod(req).flatMap {
case Some(m: String) => parseMethod.andThen(processRequestWithMethod(req, _)).apply(m)
case None => processRequestWithOriginalMethod(req)
}

Kleisli { req: Request[G] =>
{
config.overridableMethods
.contains(req.method)
.guard[Option]
.as(processRequest(req))
.getOrElse(processRequestWithOriginalMethod(req))
}
}
}
}
Oops, something went wrong.
ProTip! Use n and p to navigate between commits in a pull request.
You can鈥檛 perform that action at this time.