-
Notifications
You must be signed in to change notification settings - Fork 96
/
Copy pathHttp.scala
389 lines (321 loc) · 15.5 KB
/
Http.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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
package com.evolutiongaming.bootcamp.http
import cats.data.{EitherT, Validated}
import cats.effect.{Clock, ExitCode, IO, IOApp}
import cats.syntax.all._
import com.comcast.ip4s._
import com.evolutiongaming.bootcamp.http.Protocol._
import org.http4s._
import org.http4s.ember.client._
import org.http4s.ember.server._
import org.http4s.client.dsl.io._
import org.http4s.dsl.io._
import org.http4s.headers._
import org.http4s.implicits._
import org.http4s.multipart.{Multipart, Multiparts, Part}
import org.http4s.server.middleware.ErrorHandling
import org.typelevel.ci.CIString
import java.time.{Instant, LocalDate}
import scala.concurrent.ExecutionContext
import scala.util.Try
object HttpIntroduction {
// HTTP
// HTTP (HyperText Transfer Protocol) is the foundation of data exchange on the web. It is an application
// layer client-server protocol. The client (i.e. a web browser or some other application) initiates
// requests and receives responses back from the server.
// HTTP evolves over time. The most widely used versions as of now are HTTP/1.1 and HTTP/2. Both versions
// use TCP (Transmission Control Protocol) as their transport. HTTP/3, currently in development, will switch
// to using QUIC instead. We will not cover differences between HTTP versions in this lecture.
// If still interested, see:
// https://blog.cloudflare.com/http3-the-past-present-and-future/
// HTTP is not secure by default. To make HTTP connections secure, one has to use HTTPS, an extension of
// HTTP protocol that uses TLS (Transport Layer Security) for encryption and authentication purposes. Again,
// this will not be covered in this lecture. If still interested, see:
// https://www.ssl.com/faqs/what-is-https/
// HTTP OVERVIEW
// 1. HTTP is simple. HTTP messages can be read and understood by humans.
// Sample HTTP request:
//
// GET / HTTP/1.1
// Host: www.google.com
// Accept-Language: en
// Sample HTTP response:
//
// HTTP/1.1 200 OK
// Date: Sat, 09 Oct 2010 14:28:02 GMT
// Content-Length: 29769
// Content-Type: text/html
//
// <!DOCTYPE html... (here comes the 29769 bytes of the requested web page)
// HTTP request methods:
//
// GET
// The GET method requests a representation of the specified
// resource. Requests using GET should only retrieve data and
// should have no other effect.
//
// POST
// The POST request method requests that a web server accepts the
// data enclosed in the body of the request message, most likely
// for storing it.
//
// PUT
// The PUT method requests that the enclosed entity be stored under
// the supplied URI.
//
// DELETE
// The DELETE method deletes the specified resource.
//
// PATCH
// The PATCH method applies partial modifications to a resource.
// | HTTP Method | Request Has Body | Safe | Idempotent | Cacheable |
// |-------------+------------------+------+------------+-----------|
// | GET | Optional | Yes | Yes | Yes |
// | POST | Yes | No | No | Yes |
// | PUT | Yes | No | Yes | No |
// | DELETE | Optional | No | Yes | No |
// | PATCH | Yes | No | No | No |
// HTTP response status codes:
//
// 1xx Informational
// 2xx Successful
// 3xx Redirection
// 4xx Client Error
// 5xx Server Error
// 2. HTTP is easily extensible. For example, via custom HTTP headers.
// 3. HTTP is stateless, but not sessionless. Each HTTP request is independent. It has no links to other
// requests sent over the same connection. However, HTTP supports cookies (small pieces of data stored by
// the client and passed alongside related HTTP requests), which allow the use of stateful sessions.
// 4. HTTP resources are identified and located on the network via URLs (Uniform Resource Locators):
//
// userinfo host port
// ┌───────┴───────┐ ┌────┴────────┐ ┌┴┐
// http://john.doe:[email protected]:123/forum/questions/?tag=networking&order=newest#top
// └─┬───┘└──────────┬────────────────────────┘└─┬─────────────┘└────────┬─────────────────┘└┬─┘
// scheme authority path query fragment
}
// Models that are shared between `HttpServer` and `HttpClient` below.
object Protocol {
final case class User(name: String, age: Int)
final case class Greeting(text: String, timestamp: Instant)
}
object HttpServer extends IOApp {
// Http4s is a type safe, purely functional, streaming HTTP library for Scala.
// Let's build an HTTP server using this library powered by Cats Effect IO.
// SIMPLE GET AND POST REQUESTS
private val helloRoutes = HttpRoutes.of[IO] {
// curl "localhost:9001/hello/world"
case GET -> Root / "hello" / name =>
Ok(s"Hello, $name!")
// curl -XPOST "localhost:9001/hello" -d "world"
case req @ POST -> Root / "hello" =>
Ok(req.as[String].map(name => s"Hello again, $name!"))
}
// PATH AND QUERY PARAMETERS
private val paramsRoutes = {
// URL path parameters are strings by default. But they can be extracted and converted to any specific
// type via a custom extractor object. By convention the extractor of a value of type `T` must implement
// the following method: `def unapply(value: String): Option[T]`.
//
// Http4s provides a few predefined extractor objects, others must be supplied manually:
// import org.http4s.dsl.io.{IntVar, LongVar, UUIDVar}
object LocalDateVar {
def unapply(value: String): Option[LocalDate] =
Try(LocalDate.parse(value)).toOption
}
// URL query parameters must have corresponding `QueryParamDecoderMatcher` objects to extract them.
// `QueryParamDecoderMatcher[T]` in turn needs an `implicit QueryParamDecoder[T]` in scope that implements
// the actual extraction logic. Http4s contains a number of predefined `QueryParamDecoder` instances for
// simple types, others must be supplied manually.
implicit val localDateDecoder: QueryParamDecoder[LocalDate] = { param =>
Validated
.catchNonFatal(LocalDate.parse(param.value))
.leftMap(t => ParseFailure(s"Failed to decode LocalDate", t.getMessage))
.toValidatedNel
}
object LocalDateMatcher extends QueryParamDecoderMatcher[LocalDate](name = "date")
HttpRoutes.of[IO] {
// curl "localhost:9001/params/2020-11-10"
case GET -> Root / "params" / LocalDateVar(localDate) =>
Ok(s"Matched path param: $localDate")
// curl "localhost:9001/params?date=2020-11-10"
case GET -> Root / "params" :? LocalDateMatcher(localDate) =>
Ok(s"Matched query param: $localDate")
// Exercise 1. Implement HTTP endpoint that validates the provided timestamp in ISO-8601 format. If valid,
// 200 OK status must be returned with "Timestamp is valid" string in the body. If not valid,
// 400 Bad Request status must be returned with "Timestamp is invalid" string in the body.
// curl "localhost:9001/params/validate?timestamp=2020-11-04T14:19:54.736Z"
}
}
// HEADERS AND COOKIES
// There are limitations to what characters can be used in HTTP headers and cookies. The exact limitations
// are tricky, we will not cover them in detail. The safest options are:
// * in headers to use US-ASCII characters only;
// * in cookies to use US-ASCII characters only, excluding whitespace, double quote, comma, semicolon and backslash.
private val headersRoutes = HttpRoutes.of[IO] {
// curl "localhost:9001/headers" -H "Request-Header: Request header value"
case req @ GET -> Root / "headers" =>
Ok(s"Received headers: ${req.headers}", Header.Raw(CIString("Response-Header"), "Response header value"))
// Exercise 2. Implement HTTP endpoint that attempts to read the value of the cookie named "counter". If
// present and contains an integer value, it should add 1 to the value and request the client to update
// the cookie. Otherwise it should request the client to store "1" in the "counter" cookie.
// curl -v "localhost:9001/cookies" -b "counter=9"
}
// JSON ENTITIES
// Http4s provides integration with the following JSON libraries out of the box: Circe, Argonaut and Json4s.
// With some boilerplate it is possible to integrate Http4s with any other library that does entity
// serialization to and from JSON, XML, other formats.
private val jsonRoutes = {
import io.circe.generic.auto._
import org.http4s.circe.CirceEntityCodec._
// User JSON decoder can also be declared explicitly instead of importing from `CirceEntityCodec`:
// implicit val userDecoder: EntityDecoder[IO, User] = org.http4s.circe.jsonOf[IO, User]
HttpRoutes.of[IO] {
// curl -XPOST "localhost:9001/json" -d '{"name": "John", "age": 18}' -H "Content-Type: application/json"
case req @ POST -> Root / "json" =>
for {
user <- req.as[User]
timestamp <- Clock[IO].realTimeInstant
greeting = Greeting(s"Hello, ${user.name}!", timestamp)
response <- Ok(greeting)
} yield response
}
}
// CUSTOM ENTITY DECODERS
// It is possible to define custom entity decoders for HTTP request bodies.
private val entityRoutes = {
implicit val userDecoder: EntityDecoder[IO, User] = EntityDecoder
.decodeBy(MediaType.text.plain) { m: Media[IO] =>
val NameRegex = """\((.*),(\d{1,3})\)""".r
EitherT {
m.as[String].map {
case NameRegex(name, age) => User(name, age.toInt).asRight
case s => InvalidMessageBodyFailure(s"Invalid value: $s").asLeft
}
}
}
HttpRoutes.of[IO] {
// curl -XPOST 'localhost:9001/entity' -d '(John,18)'
case req @ POST -> Root / "entity" =>
req.as[User].flatMap(user => Ok(s"Hello, ${user.name}!"))
}
}
// MULTIPART REQUESTS
// Multipart requests combine multiple sets of data. For example, they can be used to transfer text strings
// and binary files simultaneously, which is often handy when submitting web forms.
private val multipartRoutes = HttpRoutes.of[IO] {
// Exercise 3. Implement HTTP endpoint that processes a multipart request. The request is expected to have
// the following parts:
// 1. character - contains a single character;
// 2. file - contains a text file.
//
// The endpoint should count how many times the given character is present in the file (case-sensitive)
// and return that number back in OK 200 response. 400 Bad Request response with an empty body is
// expected if the request is invalid for any reason.
// curl -XPOST "localhost:9001/multipart" -F "character=n" -F [email protected]
case req @ POST -> Root / "multipart" =>
req.as[Multipart[IO]].flatMap { multipart =>
???
}
}
private[http] val httpApp = ErrorHandling {
Seq(
helloRoutes,
paramsRoutes,
headersRoutes,
jsonRoutes,
entityRoutes,
multipartRoutes,
).reduce(_ <+> _)
}.orNotFound
override def run(args: List[String]): IO[ExitCode] =
EmberServerBuilder
.default[IO]
.withHost(ipv4"127.0.0.1")
.withPort(port"9001")
.withHttpApp(httpApp)
.build
.useForever
}
object HttpClient extends IOApp {
// Now let's build an HTTP client using Http4s. It will call endpoints, exposed by the HTTP server above.
private val uri = uri"http://localhost:9001"
private def printLine(string: String = ""): IO[Unit] = IO(println(string))
def run(args: List[String]): IO[ExitCode] =
EmberClientBuilder
.default[IO]
.build
.use { client =>
for {
_ <- printLine(string = "Executing simple GET and POST requests:")
_ <- client.expect[String](uri / "hello" / "world") >>= printLine
_ <- client.expect[String](Method.POST("world", uri / "hello")) >>= printLine
_ <- printLine()
_ <- printLine(string = "Executing requests with path and query parameters:")
_ <- client.expect[String](uri / "params" / "2020-11-10") >>= printLine
_ <- client.expect[String]((uri / "params").withQueryParam(key = "date", value = "2020-11-10")) >>= printLine
// Exercise 4. Call HTTP endpoint, implemented in scope of Exercise 1, and print the response body.
// curl "localhost:9001/params/validate?timestamp=2020-11-04T14:19:54.736Z"
_ <- printLine()
_ <- for {
_ <- printLine(string = "Executing request with headers and cookies:")
request = Method.GET(
uri / "headers",
Headers(Header.Raw(CIString("Request-Header"), "Request header value")),
)
// `client.run()` provides more flexibility than `client.expect()`, since the entire response
// becomes available for consumption and processing as `Resource[IO, Response[IO]]`.
response <- client.run(request).use { response =>
response.bodyText.compile.string.map { bodyString =>
s"""Response body is:
|$bodyString
|Response headers are:
|${response.headers}""".stripMargin
}
}
_ <- printLine(response)
} yield ()
// Exercise 5. Call HTTP endpoint, implemented in scope of Exercise 2, and print the response cookie.
// curl -v "localhost:9001/cookies" -b "counter=9"
_ <- printLine()
_ <- printLine(string = "Executing request with JSON entities:")
_ <- {
import io.circe.generic.auto._
import org.http4s.circe.CirceEntityCodec._
// User JSON encoder can also be declared explicitly instead of importing from `CirceEntityCodec`:
// implicit val helloEncoder = org.http4s.circe.jsonEncoderOf[IO, User]
client
.expect[Greeting](Method.POST(User("John", 18), uri / "json"))
.flatMap(greeting => printLine(greeting.toString))
}
_ <- printLine()
_ <- printLine(string = "Executing request with custom encoded entities:")
_ <- {
implicit val encoder: EntityEncoder[IO, User] = EntityEncoder
.stringEncoder[IO]
.contramap { user: User =>
s"(${user.name},${user.age})"
}
client.expect[String](Method.POST(User("John", 18), uri / "entity")) >>= printLine
}
_ <- printLine()
_ <- printLine(string = "Executing multipart requests:")
multiparts <- Multiparts.forSync[IO]
file = getClass.getResource("/text.txt")
multipart <- multiparts.multipart(
Vector(
Part.formData("character", "n"),
Part.fileData("file", file, `Content-Type`(MediaType.text.plain)),
)
)
_ <- client.expect[String](
Method.POST(multipart, uri / "multipart").withHeaders(multipart.headers)
) >>= printLine
_ <- printLine()
} yield ()
}
.as(ExitCode.Success)
}
// Attributions and useful links:
// https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Overview
// https://http4s.org/latest/dsl/