-
Notifications
You must be signed in to change notification settings - Fork 787
/
MessageFailure.scala
157 lines (127 loc) · 5.71 KB
/
MessageFailure.scala
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
/*
* Copyright 2013 http4s.org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.http4s
import cats.Eq
import cats.MonadError
import cats.instances.either._
import cats.parse.Parser0
import cats.syntax.all._
import scala.util.control.NoStackTrace
import scala.util.control.NonFatal
/** Indicates a failure to handle an HTTP [[Message]]. */
trait MessageFailure extends RuntimeException {
/** Provides a message appropriate for logging. */
def message: String
/* Overridden for sensible logging of the failure */
override final def getMessage: String = message
def cause: Option[Throwable]
override final def getCause: Throwable = cause.orNull
/** Provides a default rendering of this failure as a [[Response]]. */
def toHttpResponse[F[_]](httpVersion: HttpVersion): Response[F]
}
/** Indicates an error parsing an HTTP [[Message]].
*
* @param sanitized May safely be displayed to a client to describe an error
* condition. Should not echo any part of a Request.
* @param details Contains any relevant details omitted from the sanitized
* version of the error. This may freely echo a Request.
*/
final case class ParseFailure(sanitized: String, details: String)
extends MessageFailure
with NoStackTrace {
def message: String =
if (sanitized.isEmpty) details
else if (details.isEmpty) sanitized
else s"$sanitized: $details"
def cause: Option[Throwable] = None
def toHttpResponse[F[_]](httpVersion: HttpVersion): Response[F] =
Response(Status.BadRequest, httpVersion)
.withEntity(sanitized)(EntityEncoder.stringEncoder[F])
}
object ParseFailure {
implicit val eq: Eq[ParseFailure] = Eq.fromUniversalEquals[ParseFailure]
}
object ParseResult {
def fail(sanitized: String, details: String): ParseResult[Nothing] =
Left(ParseFailure(sanitized, details))
def success[A](a: A): ParseResult[A] =
Right(a)
def fromTryCatchNonFatal[A](sanitized: String)(f: => A): ParseResult[A] =
try ParseResult.success(f)
catch {
case NonFatal(e) => Left(ParseFailure(sanitized, e.getMessage))
}
private[http4s] def fromParser[A](parser: Parser0[A], errorMessage: => String)(
s: String
): ParseResult[A] =
try parser.parseAll(s).leftMap(e => ParseFailure(errorMessage, e.show))
catch { case p: ParseFailure => p.asLeft[A] }
implicit val parseResultMonad: MonadError[ParseResult, ParseFailure] =
catsStdInstancesForEither[ParseFailure]
}
/** Indicates a problem decoding a [[Message]]. This may either be a problem with
* the entity headers or with the entity itself.
*/
trait DecodeFailure extends MessageFailure
object DecodeFailure {
implicit val http4sEqForDecodeFailure: Eq[DecodeFailure] = Eq.fromUniversalEquals
}
/** Indicates a problem decoding a [[Message]] body. */
trait MessageBodyFailure extends DecodeFailure
/** Indicates an syntactic error decoding the body of an HTTP [[Message]]. */
final case class MalformedMessageBodyFailure(details: String, cause: Option[Throwable] = None)
extends MessageBodyFailure {
def message: String =
s"Malformed message body: $details"
def toHttpResponse[F[_]](httpVersion: HttpVersion): Response[F] =
Response(Status.BadRequest, httpVersion)
.withEntity(s"The request body was malformed.")(EntityEncoder.stringEncoder[F])
}
/** Indicates a semantic error decoding the body of an HTTP [[Message]]. */
final case class InvalidMessageBodyFailure(details: String, cause: Option[Throwable] = None)
extends MessageBodyFailure {
def message: String =
s"Invalid message body: $details"
def toHttpResponse[F[_]](httpVersion: HttpVersion): Response[F] =
Response(Status.UnprocessableEntity, httpVersion)
.withEntity(s"The request body was invalid.")(EntityEncoder.stringEncoder[F])
}
/** Indicates that a [[Message]] came with no supported [[MediaType]]. */
sealed abstract class UnsupportedMediaTypeFailure extends DecodeFailure with NoStackTrace {
def expected: Set[MediaRange]
def cause: Option[Throwable] = None
protected def sanitizedResponsePrefix: String
protected def expectedMsg: String =
s"Expected one of the following media ranges: ${expected.iterator.map(_.show).mkString(", ")}"
protected def responseMsg: String = s"$sanitizedResponsePrefix. $expectedMsg"
def toHttpResponse[F[_]](httpVersion: HttpVersion): Response[F] =
Response(Status.UnsupportedMediaType, httpVersion)
.withEntity(responseMsg)(EntityEncoder.stringEncoder[F])
}
/** Indicates that a [[Message]] attempting to be decoded has no [[MediaType]] and no
* [[EntityDecoder]] was lenient enough to accept it.
*/
final case class MediaTypeMissing(expected: Set[MediaRange]) extends UnsupportedMediaTypeFailure {
def sanitizedResponsePrefix: String = "No media type specified in Content-Type header"
def message: String = responseMsg
}
/** Indicates that no [[EntityDecoder]] matches the [[MediaType]] of the [[Message]] being decoded */
final case class MediaTypeMismatch(messageType: MediaType, expected: Set[MediaRange])
extends UnsupportedMediaTypeFailure {
def sanitizedResponsePrefix: String =
"Media type supplied in Content-Type header is not supported"
def message: String = s"${messageType.show} is not a supported media type. $expectedMsg"
}