实践Scala的时候需要用Http Client,找到这篇Comparing Scala’s HTTP client librariesscalaj-http用起来最顺手,就在项目中用上了,但是后来发现scalaj-http不支持PATCH,而文章作者提到的Newman已经很久没有更新了,spray-client依赖于akkaplay-ws本身的目标用户很大部分是play的用户。所以最后的选择是使用Java实现的OKHttp

在寻找称手的Http Client的时候也找到过http4s,但是http4s的文档花了很大的篇幅介绍如何用http4s构建Http Server,在介绍http4s的client时只介绍了如何发起一个HTTP GET请求。便没有细看。前段时间在Scala China的微信群里面讨论HTTP Client时,hepin提到可以试试http4s,于是翻了源码,学习了一下http4s的简单使用。http4s的文档实在简陋,故而记录如下:

Demo

Dependencies

1
2
3
libraryDependencies ++= Seq(
"org.http4s" %% "http4s-blaze-client" % "0.14.11a"
)

Http Get with Params

1
2
3
4
5
6
7
val client = PooledHttp1Client()
val httpize = Uri.uri("http://httpize.herokuapp.com")
// val url = Url.fromString("http://httpize.herokuapp.com/cookies/set?name=rendong&name=shaobo")

val url = httpize / "cookies" / "set" +? ("name", Seq("rendong", "shaobo"))
val task = client.expect[String](url)
val body = task.unsafePerformSync

Error Handling

1
2
3
4
5
6
7
val either = task.attempt.unsafePerformSync
either match {
case -\/(e) =>
e.printStackTrace()
case \/-(body) =>
println(body)
}

这里面的task实际上是scalaz的Task,either的类型是\/,是scalaz的Either实现版本。

Http Post

1
2
3
4
5
6
7
8
val req = Request(method = Method.POST, uri = httpize / "post").withBody("hello")
val task = client.expect[String](req)
task.attempt.unsafePerformSync match {
case -\/(e) =>
e.printStackTrace()
case \/-(body) =>
println(body)
}

http4s也支持PATCH, PUT, DELETE等操作,具体和Http Post的代码类似。

Response

1
2
3
4
5
6
7
8
9
10
11
12
13
14
val req = Request(method = Method.PATCH, uri = httpize / "post")
.withBody("patch")
.asInstanceOf[Task[Request]]
val task = client.fetch(req) {
case Ok(response) =>
Task(response)
case response => Task(response)
}
val response = task.unsafePerformSync
println(s"header: ${response.headers}")
println(s"status: ${response.status}")
println(s"content-type: ${response.contentType}")
println(s"content-length: ${response.contentLength}")
println(s"body: ${response.body}")

运行的结果是:

1
2
3
4
5
header: Headers(Server: Cowboy, Connection: keep-alive, Content-Type: text/plain; charset=UTF-8, Content-Length: 20, Date: Sat, 26 Nov 2016 11:26:55 GMT, Via: 1.1 vegur)
status: 404 Not Found
content-type: Some(Content-Type: text/plain; charset=UTF-8)
content-length: Some(20)
body: Emit(Vector(ByteVector(20 bytes, 0x343034204e6f7420466f756e643a202f706f7374)))

这里面值得注意的是withBody得到的类型其实是Task[Request#Self](见参考文献)。如果你希望得到整个Response而不仅仅关注于response body,你就需要用fetch而不是expectgetfetch的特例,即GET请求的fetch

Body

上一节中拿到的body的类型是EntityBody,翻阅源码后发现其类型实际上是Process[Task, ByteVector]。涉及scalaz,就比较令人头疼了。读http4s的测试用例,就可以知道可以这样处理:

1
2
3
4
5
6
7
8
9
10
def getBody(body: EntityBody): String =
(body.runLog.map(_.reduce(_ ++ _).toArray))
.unsafePerformSync
.map(_.asInstanceOf[Char])
.mkString

println(s"decoded: ${getBody(response.body)}")

// result:
// decoded: 404 Not Found: /post

References

  1. Getting started doc for http4s
  2. scala类型系统:9) this别名&自身类型