Merge pull request #15967 from spray/w/15635-http-remove-all-global-state
!hco #15635 reorganize registration of predefined, custom HTTP primitives + other small improvements
This commit is contained in:
commit
fb96a2964b
23 changed files with 128 additions and 105 deletions
|
|
@ -6,7 +6,7 @@ package akka.http.model.japi;
|
|||
|
||||
/**
|
||||
* Represents a charset in Http. See {@link HttpCharsets} for a set of predefined charsets and
|
||||
* static constructors to create and register custom charsets.
|
||||
* static constructors to create custom charsets.
|
||||
*/
|
||||
public abstract class HttpCharset {
|
||||
/**
|
||||
|
|
@ -34,7 +34,7 @@ public abstract class HttpCharset {
|
|||
public abstract HttpCharsetRange withQValue(float qValue);
|
||||
|
||||
/**
|
||||
* Returns the predefined (and registered) alias names for this charset.
|
||||
* Returns the predefined alias names for this charset.
|
||||
*/
|
||||
public abstract Iterable<String> getAliases();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,13 +21,10 @@ public final class HttpCharsets {
|
|||
public static final HttpCharset UTF_16LE = akka.http.model.HttpCharsets.UTF$minus16LE();
|
||||
|
||||
/**
|
||||
* Registers a custom charset. Returns Some(newCharset) if the charset is supported by this JVM.
|
||||
* Returns None otherwise.
|
||||
* Create and return a custom charset.
|
||||
*/
|
||||
public static Option<HttpCharset> registerCustom(String value, String... aliases) {
|
||||
scala.Option<akka.http.model.HttpCharset> custom = akka.http.model.HttpCharset.custom(value, Util.<String, String>convertArray(aliases));
|
||||
if (custom.isDefined()) return Option.<HttpCharset>some(akka.http.model.HttpCharsets.register(custom.get()));
|
||||
else return Option.none();
|
||||
public static HttpCharset custom(String value, String... aliases) {
|
||||
return akka.http.model.HttpCharset.custom(value, Util.<String, String>convertArray(aliases));
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ package akka.http.model.japi;
|
|||
|
||||
/**
|
||||
* Represents an HTTP request method. See {@link HttpMethods} for a set of predefined methods
|
||||
* and static constructors to build and register custom ones.
|
||||
* and static constructors to create custom ones.
|
||||
*/
|
||||
public abstract class HttpMethod {
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -24,12 +24,15 @@ public final class HttpMethods {
|
|||
public static final HttpMethod TRACE = akka.http.model.HttpMethods.TRACE();
|
||||
|
||||
/**
|
||||
* Register a custom method type.
|
||||
* Create a custom method type.
|
||||
*/
|
||||
public static HttpMethod registerCustom(String value, boolean safe, boolean idempotent, boolean entityAccepted) {
|
||||
return akka.http.model.HttpMethods.register(akka.http.model.HttpMethod.custom(value, safe, idempotent, entityAccepted));
|
||||
public static HttpMethod custom(String value, boolean safe, boolean idempotent, boolean entityAccepted) {
|
||||
return akka.http.model.HttpMethod.custom(value, safe, idempotent, entityAccepted);
|
||||
}
|
||||
|
||||
/**
|
||||
* Looks up a predefined HTTP method with the given name.
|
||||
*/
|
||||
public static Option<HttpMethod> lookup(String name) {
|
||||
return Util.<HttpMethod, akka.http.model.HttpMethod>lookupInRegistry(HttpMethods$.MODULE$, name);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -179,16 +179,16 @@ public abstract class MediaTypes {
|
|||
public static final MediaType VIDEO_WEBM = akka.http.model.MediaTypes.video$divwebm();
|
||||
|
||||
/**
|
||||
* Register a custom media type.
|
||||
* Creates a custom media type.
|
||||
*/
|
||||
public static MediaType registerCustom(
|
||||
public static MediaType custom(
|
||||
String mainType,
|
||||
String subType,
|
||||
boolean compressible,
|
||||
boolean binary,
|
||||
Iterable<String> fileExtensions,
|
||||
Map<String, String> params) {
|
||||
return akka.http.model.MediaTypes.register(akka.http.model.MediaType.custom(mainType, subType, compressible, binary, Util.<String, String>convertIterable(fileExtensions), Util.convertMapToScala(params), false));
|
||||
return akka.http.model.MediaType.custom(mainType, subType, compressible, binary, Util.<String, String>convertIterable(fileExtensions), Util.convertMapToScala(params), false);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -85,17 +85,17 @@ public final class StatusCodes {
|
|||
public static final StatusCode NETWORK_CONNECT_TIMEOUT = akka.http.model.StatusCodes.NetworkConnectTimeout();
|
||||
|
||||
/**
|
||||
* Registers a custom status code.
|
||||
* Create a custom status code.
|
||||
*/
|
||||
public static StatusCode registerCustom(int intValue, String reason, String defaultMessage, boolean isSuccess, boolean allowsEntity) {
|
||||
return akka.http.model.StatusCodes.registerCustom(intValue, reason, defaultMessage, isSuccess, allowsEntity);
|
||||
public static StatusCode custom(int intValue, String reason, String defaultMessage, boolean isSuccess, boolean allowsEntity) {
|
||||
return akka.http.model.StatusCodes.custom(intValue, reason, defaultMessage, isSuccess, allowsEntity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a custom status code.
|
||||
* Create a custom status code.
|
||||
*/
|
||||
public static StatusCode registerCustom(int intValue, String reason, String defaultMessage) {
|
||||
return akka.http.model.StatusCodes.registerCustom(intValue, reason, defaultMessage);
|
||||
public static StatusCode custom(int intValue, String reason, String defaultMessage) {
|
||||
return akka.http.model.StatusCodes.custom(intValue, reason, defaultMessage);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ akka.http {
|
|||
parsing {
|
||||
# The limits for the various parts of the HTTP message parser.
|
||||
max-uri-length = 2k
|
||||
max-method-length = 16
|
||||
max-response-reason-length = 64
|
||||
max-header-name-length = 64
|
||||
max-header-value-length = 8k
|
||||
|
|
|
|||
|
|
@ -40,18 +40,20 @@ private[http] class HttpRequestParser(_settings: ParserSettings,
|
|||
|
||||
def parseMethod(input: ByteString, cursor: Int): Int = {
|
||||
@tailrec def parseCustomMethod(ix: Int = 0, sb: JStringBuilder = new JStringBuilder(16)): Int =
|
||||
if (ix < 16) { // hard-coded maximum custom method length
|
||||
if (ix < maxMethodLength) {
|
||||
byteChar(input, cursor + ix) match {
|
||||
case ' ' ⇒
|
||||
HttpMethods.getForKey(sb.toString) match {
|
||||
customMethods(sb.toString) match {
|
||||
case Some(m) ⇒
|
||||
method = m
|
||||
cursor + ix + 1
|
||||
case None ⇒ parseCustomMethod(Int.MaxValue, sb)
|
||||
case None ⇒ throw new ParsingException(NotImplemented, ErrorInfo("Unsupported HTTP method", sb.toString))
|
||||
}
|
||||
case c ⇒ parseCustomMethod(ix + 1, sb.append(c))
|
||||
}
|
||||
} else throw new ParsingException(NotImplemented, ErrorInfo("Unsupported HTTP method", sb.toString))
|
||||
} else throw new ParsingException(BadRequest,
|
||||
ErrorInfo("Unsupported HTTP method", s"HTTP method too long (started with '${sb.toString}'). " +
|
||||
"Increase `akka.http.server.parsing.max-method-length` to support HTTP methods with more characters."))
|
||||
|
||||
@tailrec def parseMethod(meth: HttpMethod, ix: Int = 1): Int =
|
||||
if (ix == meth.value.length)
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ private[http] class HttpResponseParser(_settings: ParserSettings,
|
|||
case 200 ⇒ StatusCodes.OK
|
||||
case _ ⇒ StatusCodes.getForKey(code) match {
|
||||
case Some(x) ⇒ x
|
||||
case None ⇒ badStatusCode
|
||||
case None ⇒ customStatusCodes(code) getOrElse badStatusCode
|
||||
}
|
||||
}
|
||||
cursor + 4
|
||||
|
|
|
|||
|
|
@ -6,11 +6,12 @@ package akka.http.engine.parsing
|
|||
|
||||
import com.typesafe.config.Config
|
||||
import scala.collection.JavaConverters._
|
||||
import akka.http.model.Uri
|
||||
import akka.http.model.{ StatusCode, HttpMethod, Uri }
|
||||
import akka.http.util._
|
||||
|
||||
final case class ParserSettings(
|
||||
maxUriLength: Int,
|
||||
maxMethodLength: Int,
|
||||
maxResponseReasonLength: Int,
|
||||
maxHeaderNameLength: Int,
|
||||
maxHeaderValueLength: Int,
|
||||
|
|
@ -20,9 +21,12 @@ final case class ParserSettings(
|
|||
maxChunkSize: Int,
|
||||
uriParsingMode: Uri.ParsingMode,
|
||||
illegalHeaderWarnings: Boolean,
|
||||
headerValueCacheLimits: Map[String, Int]) extends HttpHeaderParser.Settings {
|
||||
headerValueCacheLimits: Map[String, Int],
|
||||
customMethods: String ⇒ Option[HttpMethod],
|
||||
customStatusCodes: Int ⇒ Option[StatusCode]) extends HttpHeaderParser.Settings {
|
||||
|
||||
require(maxUriLength > 0, "max-uri-length must be > 0")
|
||||
require(maxMethodLength > 0, "max-method-length must be > 0")
|
||||
require(maxResponseReasonLength > 0, "max-response-reason-length must be > 0")
|
||||
require(maxHeaderNameLength > 0, "max-header-name-length must be > 0")
|
||||
require(maxHeaderValueLength > 0, "max-header-value-length must be > 0")
|
||||
|
|
@ -35,6 +39,15 @@ final case class ParserSettings(
|
|||
|
||||
def headerValueCacheLimit(headerName: String): Int =
|
||||
headerValueCacheLimits.getOrElse(headerName, defaultHeaderValueCacheLimit)
|
||||
|
||||
def withCustomMethods(methods: HttpMethod*): ParserSettings = {
|
||||
val map = methods.map(m ⇒ m.name -> m).toMap
|
||||
copy(customMethods = map.get)
|
||||
}
|
||||
def withCustomStatusCodes(codes: StatusCode*): ParserSettings = {
|
||||
val map = codes.map(c ⇒ c.intValue -> c).toMap
|
||||
copy(customStatusCodes = map.get)
|
||||
}
|
||||
}
|
||||
|
||||
object ParserSettings extends SettingsCompanion[ParserSettings]("akka.http.parsing") {
|
||||
|
|
@ -43,6 +56,7 @@ object ParserSettings extends SettingsCompanion[ParserSettings]("akka.http.parsi
|
|||
|
||||
apply(
|
||||
c getIntBytes "max-uri-length",
|
||||
c getIntBytes "max-method-length",
|
||||
c getIntBytes "max-response-reason-length",
|
||||
c getIntBytes "max-header-name-length",
|
||||
c getIntBytes "max-header-value-length",
|
||||
|
|
@ -52,7 +66,9 @@ object ParserSettings extends SettingsCompanion[ParserSettings]("akka.http.parsi
|
|||
c getIntBytes "max-chunk-size",
|
||||
Uri.ParsingMode(c getString "uri-parsing-mode"),
|
||||
c getBoolean "illegal-header-warnings",
|
||||
cacheConfig.entrySet.asScala.map(kvp ⇒ kvp.getKey -> cacheConfig.getInt(kvp.getKey))(collection.breakOut))
|
||||
cacheConfig.entrySet.asScala.map(kvp ⇒ kvp.getKey -> cacheConfig.getInt(kvp.getKey))(collection.breakOut),
|
||||
_ ⇒ None,
|
||||
_ ⇒ None)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,12 @@
|
|||
|
||||
package akka.http.model
|
||||
|
||||
import java.lang.{ Iterable ⇒ JIterable }
|
||||
import language.implicitConversions
|
||||
import scala.collection.immutable
|
||||
import scala.util.Try
|
||||
import java.nio.charset.Charset
|
||||
import akka.http.util._
|
||||
import java.lang.{ Iterable ⇒ JIterable }
|
||||
|
||||
/**
|
||||
* A charset range as encountered in `Accept-Charset`. Can either be a single charset, or `*`
|
||||
|
|
@ -52,12 +53,14 @@ object HttpCharsetRange {
|
|||
|
||||
final case class HttpCharset private[http] (override val value: String)(val aliases: immutable.Seq[String])
|
||||
extends japi.HttpCharset with SingletonValueRenderable with WithQValue[HttpCharsetRange] {
|
||||
@transient private[this] var _nioCharset: Charset = Charset.forName(value)
|
||||
def nioCharset: Charset = _nioCharset
|
||||
@transient private[this] var _nioCharset: Try[Charset] = HttpCharset.findNioCharset(value)
|
||||
|
||||
/** Returns the Charset for this charset if available or throws an exception otherwise */
|
||||
def nioCharset: Charset = _nioCharset.get
|
||||
|
||||
private def readObject(in: java.io.ObjectInputStream): Unit = {
|
||||
in.defaultReadObject()
|
||||
_nioCharset = Charset.forName(value)
|
||||
_nioCharset = HttpCharset.findNioCharset(value)
|
||||
}
|
||||
|
||||
def withQValue(qValue: Float): HttpCharsetRange = HttpCharsetRange(this, qValue.toFloat)
|
||||
|
|
@ -70,17 +73,15 @@ final case class HttpCharset private[http] (override val value: String)(val alia
|
|||
}
|
||||
|
||||
object HttpCharset {
|
||||
def custom(value: String, aliases: String*): Option[HttpCharset] =
|
||||
try Some(HttpCharset(value)(immutable.Seq(aliases: _*)))
|
||||
catch {
|
||||
// per documentation all exceptions thrown by `Charset.forName` are IllegalArgumentExceptions
|
||||
case e: IllegalArgumentException ⇒ None
|
||||
}
|
||||
def custom(value: String, aliases: String*): HttpCharset =
|
||||
HttpCharset(value)(immutable.Seq(aliases: _*))
|
||||
|
||||
private[http] def findNioCharset(name: String): Try[Charset] = Try(Charset.forName(name))
|
||||
}
|
||||
|
||||
// see http://www.iana.org/assignments/character-sets
|
||||
object HttpCharsets extends ObjectRegistry[String, HttpCharset] {
|
||||
def register(charset: HttpCharset): HttpCharset = {
|
||||
private def register(charset: HttpCharset): HttpCharset = {
|
||||
charset.aliases.foreach(alias ⇒ register(alias.toRootLowerCase, charset))
|
||||
register(charset.value.toRootLowerCase, charset)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,58 +4,49 @@
|
|||
|
||||
package akka.http.model
|
||||
|
||||
import akka.http.util.{ SingletonValueRenderable, ObjectRegistry }
|
||||
import scala.collection.immutable
|
||||
import akka.http.util._
|
||||
|
||||
/**
|
||||
* The method of an HTTP request.
|
||||
* @param fingerprint unique Int value for faster equality checks (uniqueness is verified during registration)
|
||||
* @param isSafe true if the resource should not be altered on the server
|
||||
* @param isIdempotent true if requests can be safely (& automatically) repeated
|
||||
* @param isEntityAccepted true if meaning of request entities is properly defined
|
||||
*/
|
||||
final case class HttpMethod private[http] (override val value: String,
|
||||
fingerprint: Int,
|
||||
isSafe: Boolean,
|
||||
isIdempotent: Boolean,
|
||||
isEntityAccepted: Boolean) extends japi.HttpMethod with SingletonValueRenderable {
|
||||
def name = value
|
||||
|
||||
override def hashCode(): Int = fingerprint
|
||||
override def equals(obj: Any): Boolean =
|
||||
obj match {
|
||||
case m: HttpMethod ⇒ fingerprint == m.fingerprint
|
||||
case _ ⇒ false
|
||||
}
|
||||
|
||||
override def toString: String = s"HttpMethod($value)"
|
||||
}
|
||||
|
||||
object HttpMethod {
|
||||
def custom(value: String, safe: Boolean, idempotent: Boolean, entityAccepted: Boolean): HttpMethod =
|
||||
custom(value, value.##, safe, idempotent, entityAccepted)
|
||||
|
||||
def custom(value: String, fingerprint: Int, safe: Boolean, idempotent: Boolean, entityAccepted: Boolean): HttpMethod = {
|
||||
require(value.nonEmpty, "value must be non-empty")
|
||||
def custom(name: String, safe: Boolean, idempotent: Boolean, entityAccepted: Boolean): HttpMethod = {
|
||||
require(name.nonEmpty, "value must be non-empty")
|
||||
require(!safe || idempotent, "An HTTP method cannot be safe without being idempotent")
|
||||
apply(value, fingerprint, safe, idempotent, entityAccepted)
|
||||
apply(name, safe, idempotent, entityAccepted)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a custom method by name and assumes properties conservatively to be
|
||||
* safe = idempotent = false and entityAccepted = true.
|
||||
*/
|
||||
def custom(name: String): HttpMethod = custom(name, safe = false, idempotent = false, entityAccepted = true)
|
||||
}
|
||||
|
||||
object HttpMethods extends ObjectRegistry[String, HttpMethod] {
|
||||
def register(method: HttpMethod): HttpMethod = {
|
||||
require(registry.values.forall(_.fingerprint != method.fingerprint), "Method fingerprint collision")
|
||||
register(method.value, method)
|
||||
}
|
||||
private def register(method: HttpMethod): HttpMethod = register(method.value, method)
|
||||
|
||||
// format: OFF
|
||||
val CONNECT = register(HttpMethod("CONNECT", 0x01, isSafe = false, isIdempotent = false, isEntityAccepted = false))
|
||||
val DELETE = register(HttpMethod("DELETE" , 0x02, isSafe = false, isIdempotent = true , isEntityAccepted = false))
|
||||
val GET = register(HttpMethod("GET" , 0x03, isSafe = true , isIdempotent = true , isEntityAccepted = false))
|
||||
val HEAD = register(HttpMethod("HEAD" , 0x04, isSafe = true , isIdempotent = true , isEntityAccepted = false))
|
||||
val OPTIONS = register(HttpMethod("OPTIONS", 0x05, isSafe = true , isIdempotent = true , isEntityAccepted = true))
|
||||
val PATCH = register(HttpMethod("PATCH" , 0x06, isSafe = false, isIdempotent = false, isEntityAccepted = true))
|
||||
val POST = register(HttpMethod("POST" , 0x07, isSafe = false, isIdempotent = false, isEntityAccepted = true))
|
||||
val PUT = register(HttpMethod("PUT" , 0x08, isSafe = false, isIdempotent = true , isEntityAccepted = true))
|
||||
val TRACE = register(HttpMethod("TRACE" , 0x09, isSafe = true , isIdempotent = true , isEntityAccepted = false))
|
||||
val CONNECT = register(HttpMethod("CONNECT", isSafe = false, isIdempotent = false, isEntityAccepted = false))
|
||||
val DELETE = register(HttpMethod("DELETE" , isSafe = false, isIdempotent = true , isEntityAccepted = false))
|
||||
val GET = register(HttpMethod("GET" , isSafe = true , isIdempotent = true , isEntityAccepted = false))
|
||||
val HEAD = register(HttpMethod("HEAD" , isSafe = true , isIdempotent = true , isEntityAccepted = false))
|
||||
val OPTIONS = register(HttpMethod("OPTIONS", isSafe = true , isIdempotent = true , isEntityAccepted = true))
|
||||
val PATCH = register(HttpMethod("PATCH" , isSafe = false, isIdempotent = false, isEntityAccepted = true))
|
||||
val POST = register(HttpMethod("POST" , isSafe = false, isIdempotent = false, isEntityAccepted = true))
|
||||
val PUT = register(HttpMethod("PUT" , isSafe = false, isIdempotent = true , isEntityAccepted = true))
|
||||
val TRACE = register(HttpMethod("TRACE" , isSafe = true , isIdempotent = true , isEntityAccepted = false))
|
||||
// format: ON
|
||||
}
|
||||
|
|
|
|||
|
|
@ -195,9 +195,7 @@ sealed abstract class NonMultipartMediaType private[http] (_value: String, _main
|
|||
|
||||
object MediaType {
|
||||
/**
|
||||
* Allows the definition of custom media types. In order for your custom type to be properly used by the
|
||||
* HTTP layer you need to create an instance, register it via `MediaTypes.register` and use this instance in
|
||||
* your custom Marshallers and Unmarshallers.
|
||||
* Create a custom media type.
|
||||
*/
|
||||
def custom(mainType: String, subType: String, compressible: Boolean = false, binary: Boolean = false,
|
||||
fileExtensions: immutable.Seq[String] = Nil, params: Map[String, String] = Map.empty,
|
||||
|
|
@ -224,9 +222,9 @@ object MediaType {
|
|||
}
|
||||
|
||||
object MediaTypes extends ObjectRegistry[(String, String), MediaType] {
|
||||
@volatile private[this] var extensionMap = Map.empty[String, MediaType]
|
||||
private[this] var extensionMap = Map.empty[String, MediaType]
|
||||
|
||||
def register(mediaType: MediaType): MediaType = synchronized {
|
||||
private def register(mediaType: MediaType): MediaType = {
|
||||
def registerFileExtension(ext: String): Unit = {
|
||||
val lcExt = ext.toRootLowerCase
|
||||
require(!extensionMap.contains(lcExt), s"Extension '$ext' clash: media-types '${extensionMap(lcExt)}' and '$mediaType'")
|
||||
|
|
|
|||
|
|
@ -62,23 +62,22 @@ object StatusCodes extends ObjectRegistry[Int, StatusCode] {
|
|||
}
|
||||
|
||||
/**
|
||||
* Create and register a custom status code and allow full customization of behavior. The value of `allowsEntity`
|
||||
* Create a custom status code and allow full customization of behavior. The value of `allowsEntity`
|
||||
* changes the parser behavior: If it is set to true, a response with this status code is required to include a
|
||||
* `Content-Length` header to be parsed correctly when keep-alive is enabled (which is the default in HTTP/1.1).
|
||||
* If `allowsEntity` is false, an entity is never expected.
|
||||
*/
|
||||
def registerCustom(intValue: Int, reason: String, defaultMessage: String, isSuccess: Boolean, allowsEntity: Boolean): StatusCode =
|
||||
reg(CustomStatusCode(intValue)(reason, defaultMessage, isSuccess, allowsEntity))
|
||||
def custom(intValue: Int, reason: String, defaultMessage: String, isSuccess: Boolean, allowsEntity: Boolean): StatusCode =
|
||||
StatusCodes.CustomStatusCode(intValue)(reason, defaultMessage, isSuccess, allowsEntity)
|
||||
|
||||
/** Create and register a custom status code with default behavior for its value region. */
|
||||
def registerCustom(intValue: Int, reason: String, defaultMessage: String = ""): StatusCode = reg {
|
||||
/** Create a custom status code with default behavior for its value region. */
|
||||
def custom(intValue: Int, reason: String, defaultMessage: String = ""): StatusCode =
|
||||
if (100 to 199 contains intValue) Informational(intValue)(reason, defaultMessage)
|
||||
else if (200 to 299 contains intValue) Success(intValue)(reason, defaultMessage)
|
||||
else if (300 to 399 contains intValue) Redirection(intValue)(reason, defaultMessage, defaultMessage)
|
||||
else if (400 to 499 contains intValue) ClientError(intValue)(reason, defaultMessage)
|
||||
else if (500 to 599 contains intValue) ServerError(intValue)(reason, defaultMessage)
|
||||
else throw new IllegalArgumentException("Can't register status code in non-standard region")
|
||||
}
|
||||
|
||||
import Informational.{apply => i}
|
||||
import Success .{apply => s}
|
||||
|
|
|
|||
|
|
@ -48,11 +48,6 @@ object HttpEncoding {
|
|||
|
||||
// see http://www.iana.org/assignments/http-parameters/http-parameters.xml
|
||||
object HttpEncodings extends ObjectRegistry[String, HttpEncoding] {
|
||||
def register(encoding: HttpEncoding): HttpEncoding =
|
||||
register(encoding.value.toRootLowerCase, encoding)
|
||||
|
||||
private def register(value: String): HttpEncoding = register(HttpEncoding(value))
|
||||
|
||||
// format: OFF
|
||||
val compress = register("compress")
|
||||
val chunked = register("chunked")
|
||||
|
|
@ -62,4 +57,7 @@ object HttpEncodings extends ObjectRegistry[String, HttpEncoding] {
|
|||
val `x-compress` = register("x-compress")
|
||||
val `x-zip` = register("x-zip")
|
||||
// format: ON
|
||||
|
||||
private def register(encoding: HttpEncoding): HttpEncoding = register(encoding.value.toRootLowerCase, encoding)
|
||||
private def register(value: String): HttpEncoding = register(HttpEncoding(value))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,5 @@ private[parser] trait CommonActions {
|
|||
def getCharset(name: String): HttpCharset =
|
||||
HttpCharsets
|
||||
.getForKeyCaseInsensitive(name)
|
||||
.orElse(HttpCharset.custom(name))
|
||||
.getOrElse(throw new ParsingException("Unsupported charset", name))
|
||||
.getOrElse(HttpCharset.custom(name))
|
||||
}
|
||||
|
|
@ -408,7 +408,7 @@ private[parser] trait CommonRules { this: Parser with StringBuilding ⇒
|
|||
token ~> { s ⇒
|
||||
HttpMethods.getForKey(s) match {
|
||||
case Some(m) ⇒ m
|
||||
case None ⇒ throw new ParsingException("Unknown HTTP method", s)
|
||||
case None ⇒ HttpMethod.custom(s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,21 +7,18 @@ package akka.http.util
|
|||
/**
|
||||
* INTERNAL API
|
||||
*
|
||||
* A registry to keep track of singleton instances similar to what
|
||||
* java.lang.Enum provides.
|
||||
* A unsynchronized registry to keep track of singleton instances similar to what
|
||||
* java.lang.Enum provides. `registry` should therefore only be used inside of singleton constructors.
|
||||
*/
|
||||
private[http] trait ObjectRegistry[K, V <: AnyRef] {
|
||||
@volatile private[this] var _registry = Map.empty[K, V]
|
||||
private[this] var _registry = Map.empty[K, V]
|
||||
|
||||
protected final def register(key: K, obj: V): obj.type = synchronized {
|
||||
protected final def register(key: K, obj: V): obj.type = {
|
||||
require(!_registry.contains(key), s"ObjectRegistry for ${getClass.getSimpleName} already contains value for $key")
|
||||
_registry = _registry.updated(key, obj)
|
||||
obj
|
||||
}
|
||||
|
||||
protected def registry: Map[K, V] = _registry
|
||||
|
||||
def getForKey(key: K): Option[V] = registry.get(key)
|
||||
def getForKey(key: K): Option[V] = _registry.get(key)
|
||||
|
||||
def getForKeyCaseInsensitive(key: String)(implicit conv: String <:< K): Option[V] =
|
||||
getForKey(conv(key.toRootLowerCase))
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ class RequestParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll {
|
|||
implicit val system = ActorSystem(getClass.getSimpleName, testConf)
|
||||
import system.dispatcher
|
||||
|
||||
val BOLT = HttpMethods.register(HttpMethod.custom("BOLT", safe = false, idempotent = true, entityAccepted = true))
|
||||
val BOLT = HttpMethod.custom("BOLT", safe = false, idempotent = true, entityAccepted = true)
|
||||
implicit val materializer = FlowMaterializer()
|
||||
|
||||
"The request parsing logic should" - {
|
||||
|
|
@ -124,6 +124,9 @@ class RequestParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll {
|
|||
}
|
||||
|
||||
"with a custom HTTP method" in new Test {
|
||||
override protected def parserSettings: ParserSettings =
|
||||
super.parserSettings.withCustomMethods(BOLT)
|
||||
|
||||
"""BOLT / HTTP/1.0
|
||||
|
|
||||
|""" should parseTo(HttpRequest(BOLT, "/", protocol = `HTTP/1.0`))
|
||||
|
|
@ -266,6 +269,14 @@ class RequestParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll {
|
|||
"GETX " should parseToError(NotImplemented, ErrorInfo("Unsupported HTTP method", "GETX"))
|
||||
}
|
||||
|
||||
"a too long HTTP method" in new Test {
|
||||
"ABCDEFGHIJKLMNOPQ " should
|
||||
parseToError(BadRequest,
|
||||
ErrorInfo(
|
||||
"Unsupported HTTP method",
|
||||
"HTTP method too long (started with 'ABCDEFGHIJKLMNOP'). Increase `akka.http.server.parsing.max-method-length` to support HTTP methods with more characters."))
|
||||
}
|
||||
|
||||
"two Content-Length headers" in new Test {
|
||||
"""GET / HTTP/1.1
|
||||
|Content-Length: 3
|
||||
|
|
@ -375,7 +386,8 @@ class RequestParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll {
|
|||
Await.result(future, 250.millis)
|
||||
}
|
||||
|
||||
private def newParser = new HttpRequestParser(ParserSettings(system), false)()
|
||||
protected def parserSettings: ParserSettings = ParserSettings(system)
|
||||
protected def newParser = new HttpRequestParser(parserSettings, false)()
|
||||
|
||||
private def compactEntity(entity: RequestEntity): Deferrable[RequestEntity] =
|
||||
entity match {
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class ResponseParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll {
|
|||
import system.dispatcher
|
||||
|
||||
implicit val materializer = FlowMaterializer()
|
||||
val ServerOnTheMove = StatusCodes.registerCustom(331, "Server on the move")
|
||||
val ServerOnTheMove = StatusCodes.custom(331, "Server on the move")
|
||||
|
||||
"The response parsing logic should" - {
|
||||
"properly parse" - {
|
||||
|
|
@ -56,6 +56,9 @@ class ResponseParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll {
|
|||
}
|
||||
|
||||
"a response with a custom status code" in new Test {
|
||||
override def parserSettings: ParserSettings =
|
||||
super.parserSettings.withCustomStatusCodes(ServerOnTheMove)
|
||||
|
||||
"""HTTP/1.1 331 Server on the move
|
||||
|Content-Length: 0
|
||||
|
|
||||
|
|
@ -233,8 +236,9 @@ class ResponseParserSpec extends FreeSpec with Matchers with BeforeAndAfterAll {
|
|||
Await.result(future, 250.millis)
|
||||
}
|
||||
|
||||
def parserSettings: ParserSettings = ParserSettings(system)
|
||||
def newParser(requestMethod: HttpMethod = GET) = {
|
||||
val parser = new HttpResponseParser(ParserSettings(system),
|
||||
val parser = new HttpResponseParser(parserSettings,
|
||||
dequeueRequestMethodForNextResponse = () ⇒ requestMethod)()
|
||||
parser
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ class ResponseRendererSpec extends FreeSpec with Matchers with BeforeAndAfterAll
|
|||
implicit val system = ActorSystem(getClass.getSimpleName, testConf)
|
||||
import system.dispatcher
|
||||
|
||||
val ServerOnTheMove = StatusCodes.registerCustom(330, "Server on the move")
|
||||
val ServerOnTheMove = StatusCodes.custom(330, "Server on the move")
|
||||
implicit val materializer = FlowMaterializer()
|
||||
|
||||
"The response preparation logic should properly render" - {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class SerializabilitySpec extends WordSpec with Matchers {
|
|||
}
|
||||
"with accept-charset" in {
|
||||
HttpRequest().withHeaders(`Accept-Charset`(HttpCharsets.`UTF-16`)) should beSerializable
|
||||
HttpRequest().withHeaders(`Accept-Charset`(HttpCharset.custom("utf8").get)) should beSerializable
|
||||
HttpRequest().withHeaders(`Accept-Charset`(HttpCharset.custom("utf8"))) should beSerializable
|
||||
}
|
||||
"with accepted encodings" in {
|
||||
HttpRequest().withHeaders(`Accept-Encoding`(HttpEncodings.chunked)) should beSerializable
|
||||
|
|
|
|||
|
|
@ -17,7 +17,8 @@ import HttpEncodings._
|
|||
import HttpMethods._
|
||||
|
||||
class HttpHeaderSpec extends FreeSpec with Matchers {
|
||||
val `application/vnd.spray` = MediaTypes.register(MediaType.custom("application/vnd.spray"))
|
||||
val `application/vnd.spray` = MediaType.custom("application/vnd.spray")
|
||||
val PROPFIND = HttpMethod.custom("PROPFIND")
|
||||
|
||||
"The HTTP header model must correctly parse and render the headers" - {
|
||||
|
||||
|
|
@ -49,6 +50,7 @@ class HttpHeaderSpec extends FreeSpec with Matchers {
|
|||
`Accept-Charset`(`ISO-8859-1`, `UTF-16` withQValue 0, HttpCharsetRange.`*` withQValue 0.8).renderedTo(
|
||||
"ISO-8859-1, UTF-16;q=0.0, *;q=0.8")
|
||||
`Accept-Charset`(`UTF-16` withQValue 0.234567).toString shouldEqual "Accept-Charset: UTF-16;q=0.235"
|
||||
"Accept-Charset: UTF-16, unsupported42" =!= `Accept-Charset`(`UTF-16`, HttpCharset.custom("unsupported42"))
|
||||
}
|
||||
|
||||
"Access-Control-Allow-Credentials" in {
|
||||
|
|
@ -61,6 +63,7 @@ class HttpHeaderSpec extends FreeSpec with Matchers {
|
|||
|
||||
"Access-Control-Allow-Methods" in {
|
||||
"Access-Control-Allow-Methods: GET, POST" =!= `Access-Control-Allow-Methods`(GET, POST)
|
||||
"Access-Control-Allow-Methods: GET, PROPFIND, POST" =!= `Access-Control-Allow-Methods`(GET, PROPFIND, POST)
|
||||
}
|
||||
|
||||
"Access-Control-Allow-Origin" in {
|
||||
|
|
@ -85,6 +88,7 @@ class HttpHeaderSpec extends FreeSpec with Matchers {
|
|||
|
||||
"Access-Control-Request-Method" in {
|
||||
"Access-Control-Request-Method: POST" =!= `Access-Control-Request-Method`(POST)
|
||||
"Access-Control-Request-Method: PROPFIND" =!= `Access-Control-Request-Method`(PROPFIND)
|
||||
}
|
||||
|
||||
"Accept-Ranges" in {
|
||||
|
|
@ -113,6 +117,7 @@ class HttpHeaderSpec extends FreeSpec with Matchers {
|
|||
"Allow" in {
|
||||
"Allow: " =!= Allow()
|
||||
"Allow: GET, PUT" =!= Allow(GET, PUT)
|
||||
"Allow: GET, PROPFIND, PUT" =!= Allow(GET, PROPFIND, PUT)
|
||||
}
|
||||
|
||||
"Authorization" in {
|
||||
|
|
@ -177,7 +182,7 @@ class HttpHeaderSpec extends FreeSpec with Matchers {
|
|||
"Content-Type: text/xml; version=3; charset=windows-1252" =!=
|
||||
`Content-Type`(ContentType(MediaType.custom("text", "xml", params = Map("version" -> "3")), HttpCharsets.getForKey("windows-1252")))
|
||||
"Content-Type: text/plain; charset=fancy-pants" =!=
|
||||
ErrorInfo("Illegal HTTP header 'Content-Type': Unsupported charset", "fancy-pants")
|
||||
`Content-Type`(ContentType(`text/plain`, HttpCharset.custom("fancy-pants")))
|
||||
"Content-Type: multipart/mixed; boundary=ABC123" =!=
|
||||
`Content-Type`(ContentType(`multipart/mixed` withBoundary "ABC123"))
|
||||
"Content-Type: multipart/mixed; boundary=\"ABC/123\"" =!=
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue