sttp
Scala向けの統一HTTPクライアントライブラリ。複数のバックエンド(Java HttpClient、Akka HTTP、http4s、OkHttp等)に対応した共通インターフェース提供。JSON処理(circe、uPickle等)、ストリーミング(fs2、ZIO Streams等)、関数型プログラミングライブラリとシームレス統合。
GitHub概要
softwaremill/sttp
The Scala HTTP client you always wanted!
トピックス
スター履歴
ライブラリ
sttp
概要
sttpは「Scalaで常に欲しかったHTTPクライアント」として開発された、Scalaエコシステムにおける次世代HTTPクライアントライブラリです。「統一されたAPI設計と多様なプログラミングパラダイム対応」を重視して設計され、同期処理、Future-based、関数型エフェクトシステム(cats-effect、ZIO、Monix等)を統一APIで使用可能。不変なリクエスト定義、プラガブルバックエンド、型安全なレスポンス処理、URI補間、豊富なエコシステム統合により、Scalaの様々なスタイルでHTTP通信を直感的かつ効率的に実現します。
詳細
sttp 2025年版(v4.0系)は、SoftwareMillとVirtusLabの共同開発による成熟したScala HTTPクライアントソリューションです。Request[T]、Backend[F]、Response[T]の3つの核心抽象化により、リクエスト記述、実行、レスポンス処理を明確に分離。Scala 2.12/2.13/3対応、JVM(Java 11+)/Scala.JS/Scala Native対応のマルチプラットフォーム設計、プラガブルバックエンド(Java HttpClient、Akka HTTP、Pekko HTTP、http4s、OkHttp等)、包括的認証サポート、ストリーミング統合、テスト支援ツールを特徴とし、開発者体験と生産性向上を重視したAPIでScala開発者の多様なニーズに対応します。
主な特徴
- 統一API設計: 同期・非同期・関数型エフェクトで同一APIによる一貫した開発体験
- 不変リクエスト定義: 型安全で再利用可能なRequest[T]によるHTTPリクエスト記述
- プラガブルバックエンド: Backend[F]抽象化による柔軟なHTTP実装切り替え
- 型安全レスポンス処理: ResponseAs[T]による強力な型安全レスポンス変換
- マルチプラットフォーム対応: JVM/Scala.JS/Scala Native統一サポート
- 豊富なエコシステム統合: JSON、ストリーミング、監視ライブラリとの緊密な統合
メリット・デメリット
メリット
- Scala生態系での最高レベルの統合性と開発者体験
- 関数型プログラミングパラダイムとの自然な親和性
- 強力な型安全性による実行時エラー大幅削減
- プラガブルアーキテクチャによる高い柔軟性とカスタマイズ性
- 包括的なドキュメントとアクティブなコミュニティサポート
- マルチプラットフォーム対応による広範囲の適用可能性
デメリット
- Scala特有の概念理解による学習コストの高さ
- 豊富な機能による初期設定の複雑性
- 関数型エフェクトシステムに対する理解要求
- Java/その他言語生態系との相互運用における制限
- 小規模プロジェクトでは過度に高機能すぎる場合
- コンパイル時間の増加可能性
参考ページ
書き方の例
インストールと基本セットアップ
// build.sbt - 基本設定
libraryDependencies ++= Seq(
"com.softwaremill.sttp.client4" %% "core" % "4.0.9"
)
// JSON処理統合版
libraryDependencies ++= Seq(
"com.softwaremill.sttp.client4" %% "core" % "4.0.9",
"com.softwaremill.sttp.client4" %% "circe" % "4.0.9", // circe統合
"io.circe" %% "circe-generic" % "0.14.7"
)
// ZIO統合版
libraryDependencies ++= Seq(
"com.softwaremill.sttp.client4" %% "core" % "4.0.9",
"com.softwaremill.sttp.client4" %% "zio" % "4.0.9",
"dev.zio" %% "zio" % "2.1.6"
)
// cats-effect統合版
libraryDependencies ++= Seq(
"com.softwaremill.sttp.client4" %% "core" % "4.0.9",
"com.softwaremill.sttp.client4" %% "cats" % "4.0.9",
"org.typelevel" %% "cats-effect" % "3.5.4"
)
package main
import sttp.client4.*
// 基本的なクライアント初期化
object SttpBasics {
// クイックスタート用(実験・学習用)
def quickExample(): Unit = {
import sttp.client4.quick.*
// グローバル同期バックエンドで即座にリクエスト
val response = quickRequest.get(uri"https://api.example.com/users").send()
println(response.body)
}
// 本格的な同期バックエンド
def syncExample(): Unit = {
val backend = DefaultSyncBackend()
val request = basicRequest
.get(uri"https://api.example.com/users")
.header("Accept", "application/json")
.header("User-Agent", "MyScalaApp/1.0")
val response = request.send(backend)
response.body match {
case Right(content) => println(s"成功: $content")
case Left(error) => println(s"エラー: $error")
}
backend.close()
}
// Future-based非同期バックエンド
def futureExample(): Unit = {
import scala.concurrent.{Future, ExecutionContext}
import scala.util.{Success, Failure}
implicit val ec: ExecutionContext = ExecutionContext.global
val backend = DefaultFutureBackend()
val request = basicRequest.get(uri"https://api.example.com/users")
val responseFuture: Future[Response[Either[String, String]]] = request.send(backend)
responseFuture.onComplete {
case Success(response) =>
println(s"レスポンス: ${response.body}")
case Failure(exception) =>
println(s"失敗: ${exception.getMessage}")
}
backend.close()
}
}
基本的なHTTPリクエスト(GET/POST/PUT/DELETE)
import sttp.client4.*
import io.circe.*
import io.circe.generic.semiauto.*
import sttp.client4.circe.*
// データクラス定義
case class User(id: Int, name: String, email: String, age: Int)
case class UserCreateRequest(name: String, email: String, age: Int)
case class UserUpdateRequest(name: Option[String], email: Option[String], age: Option[Int])
// JSON Codecの定義
implicit val userDecoder: Decoder[User] = deriveDecoder[User]
implicit val userEncoder: Encoder[User] = deriveEncoder[User]
implicit val userCreateEncoder: Encoder[UserCreateRequest] = deriveEncoder[UserCreateRequest]
implicit val userUpdateEncoder: Encoder[UserUpdateRequest] = deriveEncoder[UserUpdateRequest]
object HttpRequestExamples {
val backend = DefaultSyncBackend()
// GETリクエスト(基本)
def getUserList(): Either[String, List[User]] = {
val response = basicRequest
.get(uri"https://api.example.com/users")
.response(asJson[List[User]])
.send(backend)
response.body match {
case Right(users) => Right(users)
case Left(error) => Left(s"ユーザー一覧取得失敗: $error")
}
}
// GETリクエスト(クエリパラメータ付き)
def getUserListWithParams(page: Int, limit: Int, sortBy: String): Either[String, List[User]] = {
val response = basicRequest
.get(uri"https://api.example.com/users?page=$page&limit=$limit&sort=$sortBy")
.response(asJson[List[User]])
.send(backend)
response.body match {
case Right(users) =>
println(s"取得ユーザー数: ${users.length}")
Right(users)
case Left(error) => Left(s"ページ取得失敗: $error")
}
}
// GETリクエスト(個別取得)
def getUser(userId: Int): Either[String, User] = {
val response = basicRequest
.get(uri"https://api.example.com/users/$userId")
.response(asJson[User])
.send(backend)
response.body match {
case Right(user) => Right(user)
case Left(error) => Left(s"ユーザー取得失敗: $error")
}
}
// POSTリクエスト(ユーザー作成)
def createUser(userRequest: UserCreateRequest): Either[String, User] = {
val response = basicRequest
.post(uri"https://api.example.com/users")
.body(userRequest)
.response(asJson[User])
.send(backend)
response.body match {
case Right(user) =>
println(s"ユーザー作成成功: ID=${user.id}, Name=${user.name}")
Right(user)
case Left(error) => Left(s"ユーザー作成失敗: $error")
}
}
// PUTリクエスト(ユーザー更新)
def updateUser(userId: Int, updateRequest: UserUpdateRequest): Either[String, User] = {
val response = basicRequest
.put(uri"https://api.example.com/users/$userId")
.body(updateRequest)
.response(asJson[User])
.send(backend)
response.body match {
case Right(user) =>
println(s"ユーザー更新成功: ${user.name}")
Right(user)
case Left(error) => Left(s"ユーザー更新失敗: $error")
}
}
// DELETEリクエスト
def deleteUser(userId: Int): Either[String, Unit] = {
val response = basicRequest
.delete(uri"https://api.example.com/users/$userId")
.send(backend)
if (response.code.code == 204) {
println(s"ユーザー削除成功: ID=$userId")
Right(())
} else {
Left(s"ユーザー削除失敗: ${response.code}")
}
}
// フォームデータの送信
def submitForm(username: String, email: String, category: String): Either[String, String] = {
val response = basicRequest
.post(uri"https://api.example.com/register")
.body(Map(
"username" -> username,
"email" -> email,
"category" -> category
))
.send(backend)
response.body
}
// 使用例
def main(args: Array[String]): Unit = {
// ユーザー一覧取得
getUserListWithParams(1, 10, "created_at") match {
case Right(users) => users.foreach(user => println(s"User: ${user.name}"))
case Left(error) => println(error)
}
// ユーザー作成
val newUser = UserCreateRequest("田中太郎", "[email protected]", 30)
createUser(newUser) match {
case Right(user) => println(s"作成されたユーザー: ${user.id}")
case Left(error) => println(error)
}
backend.close()
}
}
認証とセキュリティ設定
import sttp.client4.*
import sttp.client4.wrappers.DigestAuthenticationBackend
object AuthenticationExamples {
val backend = DefaultSyncBackend()
// Basic認証
def basicAuthExample(): Unit = {
val response = basicRequest
.get(uri"https://api.example.com/protected")
.auth.basic("username", "password")
.send(backend)
println(s"Basic認証レスポンス: ${response.body}")
}
// Bearer Token認証
def bearerTokenExample(): Unit = {
val token = "your-jwt-token-here"
val response = basicRequest
.get(uri"https://api.example.com/user/profile")
.auth.bearer(token)
.send(backend)
println(s"Bearer認証レスポンス: ${response.body}")
}
// APIキー認証
def apiKeyExample(): Unit = {
val apiKey = "your-api-key"
val response = basicRequest
.get(uri"https://api.example.com/data")
.header("X-API-Key", apiKey)
.header("X-Client-ID", "your-client-id")
.send(backend)
println(s"APIキー認証レスポンス: ${response.body}")
}
// OAuth 2.0 例
case class OAuthTokenResponse(
access_token: String,
token_type: String,
expires_in: Int,
refresh_token: Option[String]
)
implicit val oauthDecoder: Decoder[OAuthTokenResponse] = deriveDecoder[OAuthTokenResponse]
def oauth2Example(): Unit = {
// アクセストークン取得
val tokenResponse = basicRequest
.post(uri"https://auth.example.com/oauth2/token")
.auth.basic("client_id", "client_secret")
.body(Map(
"grant_type" -> "client_credentials",
"scope" -> "read write"
))
.response(asJson[OAuthTokenResponse])
.send(backend)
tokenResponse.body match {
case Right(token) =>
println(s"トークン取得成功: ${token.access_token}")
// 取得したトークンでAPI呼び出し
val apiResponse = basicRequest
.get(uri"https://api.example.com/user/profile")
.auth.bearer(token.access_token)
.send(backend)
println(s"API呼び出し結果: ${apiResponse.body}")
case Left(error) =>
println(s"トークン取得失敗: $error")
}
}
// Digest認証
def digestAuthExample(): Unit = {
val digestBackend = DigestAuthenticationBackend(backend)
val response = basicRequest
.get(uri"https://api.example.com/digest-protected")
.auth.digest("username", "password")
.send(digestBackend)
println(s"Digest認証レスポンス: ${response.body}")
}
// カスタム認証ヘッダー
def customAuthExample(): Unit = {
val timestamp = System.currentTimeMillis().toString
val signature = generateSignature("api-key", "secret", timestamp)
val response = basicRequest
.get(uri"https://api.example.com/secure")
.header("X-API-Key", "your-api-key")
.header("X-Timestamp", timestamp)
.header("X-Signature", signature)
.send(backend)
println(s"カスタム認証レスポンス: ${response.body}")
}
def generateSignature(apiKey: String, secret: String, timestamp: String): String = {
// 実際の署名生成ロジック
import java.security.MessageDigest
val input = s"$apiKey$timestamp$secret"
MessageDigest.getInstance("SHA-256").digest(input.getBytes).map("%02x".format(_)).mkString
}
}
ZIO統合と関数型エフェクト
import sttp.client4.*
import sttp.client4.httpclient.zio.HttpClientZioBackend
import zio.*
import io.circe.*
import io.circe.generic.semiauto.*
import sttp.client4.circe.*
object ZIOIntegrationExample extends ZIOAppDefault {
case class User(id: Int, name: String, email: String)
case class ApiError(code: Int, message: String)
implicit val userDecoder: Decoder[User] = deriveDecoder[User]
implicit val errorDecoder: Decoder[ApiError] = deriveDecoder[ApiError]
// ZIOを使ったHTTPクライアントサービス
trait UserService {
def getUser(id: Int): Task[User]
def createUser(name: String, email: String): Task[User]
def getAllUsers(): Task[List[User]]
}
case class UserServiceImpl(backend: SttpBackend[Task, Any]) extends UserService {
def getUser(id: Int): Task[User] = {
basicRequest
.get(uri"https://api.example.com/users/$id")
.response(asJson[User])
.send(backend)
.flatMap { response =>
response.body match {
case Right(user) => ZIO.succeed(user)
case Left(error) => ZIO.fail(new RuntimeException(s"ユーザー取得失敗: $error"))
}
}
}
def createUser(name: String, email: String): Task[User] = {
val userData = Map("name" -> name, "email" -> email)
basicRequest
.post(uri"https://api.example.com/users")
.body(userData)
.response(asJson[User])
.send(backend)
.flatMap { response =>
response.body match {
case Right(user) => ZIO.succeed(user)
case Left(error) => ZIO.fail(new RuntimeException(s"ユーザー作成失敗: $error"))
}
}
}
def getAllUsers(): Task[List[User]] = {
basicRequest
.get(uri"https://api.example.com/users")
.response(asJson[List[User]])
.send(backend)
.flatMap { response =>
response.body match {
case Right(users) => ZIO.succeed(users)
case Left(error) => ZIO.fail(new RuntimeException(s"ユーザー一覧取得失敗: $error"))
}
}
}
}
// レイヤー定義
val userServiceLayer: ZLayer[SttpBackend[Task, Any], Nothing, UserService] =
ZLayer.fromFunction(UserServiceImpl.apply _)
val backendLayer: TaskLayer[SttpBackend[Task, Any]] =
HttpClientZioBackend.layer()
// メインプログラム
def run: ZIO[Any, Throwable, Unit] = {
val program = for {
userService <- ZIO.service[UserService]
// 並行処理でユーザー取得
users <- ZIO.collectAllPar(List(
userService.getUser(1),
userService.getUser(2),
userService.getUser(3)
)).catchAll { error =>
ZIO.logError(s"ユーザー取得エラー: ${error.getMessage}") *>
ZIO.succeed(List.empty[User])
}
_ <- ZIO.log(s"取得ユーザー数: ${users.length}")
// 新しいユーザー作成
newUser <- userService.createUser("田中太郎", "[email protected]").catchAll { error =>
ZIO.logError(s"ユーザー作成エラー: ${error.getMessage}") *>
ZIO.fail(error)
}
_ <- ZIO.log(s"作成されたユーザー: ${newUser.name}")
// 全ユーザー一覧取得
allUsers <- userService.getAllUsers()
_ <- ZIO.log(s"総ユーザー数: ${allUsers.length}")
} yield ()
program.provide(
userServiceLayer,
backendLayer
)
}
}
エラーハンドリングとリトライ機能
import sttp.client4.*
import sttp.client4.wrappers.{TryBackend, EitherBackend}
import scala.util.{Try, Success, Failure}
import scala.concurrent.duration.*
object ErrorHandlingExamples {
val backend = DefaultSyncBackend()
// Try-basedエラーハンドリング
def tryBasedHandling(): Unit = {
val tryBackend = TryBackend(backend)
val result: Try[Response[Either[String, String]]] = basicRequest
.get(uri"https://api.example.com/unreliable")
.send(tryBackend)
result match {
case Success(response) =>
response.body match {
case Right(content) => println(s"成功: $content")
case Left(error) => println(s"レスポンスエラー: $error")
}
case Failure(exception) =>
println(s"ネットワークエラー: ${exception.getMessage}")
}
}
// Either-basedエラーハンドリング
def eitherBasedHandling(): Unit = {
val eitherBackend = EitherBackend(backend)
val result: Either[Exception, Response[Either[String, String]]] = basicRequest
.get(uri"https://api.example.com/data")
.send(eitherBackend)
result match {
case Right(response) =>
println(s"レスポンス取得成功: ${response.code}")
case Left(exception) =>
println(s"リクエスト失敗: ${exception.getMessage}")
}
}
// カスタムレスポンスハンドリング
case class ApiError(code: Int, message: String, details: String)
implicit val apiErrorDecoder: Decoder[ApiError] = deriveDecoder[ApiError]
def customResponseHandling(): Unit = {
val response = basicRequest
.get(uri"https://api.example.com/data")
.response(asEither(asJson[ApiError], asJson[List[User]]))
.send(backend)
response.body match {
case Right(Right(users)) =>
println(s"データ取得成功: ${users.length}件")
case Right(Left(error)) =>
println(s"APIエラー: ${error.message} (コード: ${error.code})")
case Left(httpError) =>
println(s"HTTPエラー: $httpError")
}
}
// リトライメカニズム実装
def retryableRequest[T](
request: Request[T, Any],
maxRetries: Int = 3,
backoffDelay: FiniteDuration = 1.second
): Either[String, Response[T]] = {
def attempt(retriesLeft: Int): Either[String, Response[T]] = {
val response = request.send(backend)
if (response.code.code >= 500 && retriesLeft > 0) {
println(s"サーバーエラー (${response.code}), リトライ残り: $retriesLeft")
Thread.sleep(backoffDelay.toMillis)
attempt(retriesLeft - 1)
} else if (response.code.code >= 400) {
Left(s"クライアントエラー: ${response.code}")
} else {
Right(response)
}
}
attempt(maxRetries)
}
// サーキットブレーカー実装
class CircuitBreaker(threshold: Int, timeout: FiniteDuration) {
private var failureCount = 0
private var lastFailureTime: Long = 0
private var state: String = "closed" // "closed", "open", "half-open"
def execute[T](request: Request[T, Any]): Either[String, Response[T]] = {
state match {
case "open" =>
if (System.currentTimeMillis() - lastFailureTime > timeout.toMillis) {
state = "half-open"
println("サーキットブレーカー: ハーフオープン状態")
executeRequest(request)
} else {
Left("サーキットブレーカーが開いています")
}
case _ =>
executeRequest(request)
}
}
private def executeRequest[T](request: Request[T, Any]): Either[String, Response[T]] = {
val response = request.send(backend)
if (response.code.code >= 500) {
failureCount += 1
lastFailureTime = System.currentTimeMillis()
if (failureCount >= threshold) {
state = "open"
println(s"サーキットブレーカーが開きました(失敗回数: $failureCount)")
}
Left(s"サーバーエラー: ${response.code}")
} else {
// 成功時はリセット
failureCount = 0
state = "closed"
Right(response)
}
}
}
def circuitBreakerExample(): Unit = {
val circuitBreaker = new CircuitBreaker(3, 30.seconds)
for (i <- 1 to 10) {
val result = circuitBreaker.execute(
basicRequest.get(uri"https://api.example.com/unreliable")
)
result match {
case Right(response) =>
println(s"試行 $i 成功: ${response.code}")
case Left(error) =>
println(s"試行 $i 失敗: $error")
}
Thread.sleep(1000)
}
}
}
ファイル操作と高度な機能
import sttp.client4.*
import java.io.File
import java.nio.file.{Files, Paths}
object FileOperationsExamples {
val backend = DefaultSyncBackend()
// ファイルアップロード
def uploadFile(): Unit = {
val file = new File("/path/to/document.pdf")
val response = basicRequest
.post(uri"https://api.example.com/upload")
.multipartBody(
multipart("file", file).fileName("document.pdf").contentType("application/pdf"),
multipart("description", "重要なドキュメント"),
multipart("category", "legal")
)
.header("Authorization", "Bearer your-token")
.send(backend)
response.body match {
case Right(result) => println(s"アップロード成功: $result")
case Left(error) => println(s"アップロード失敗: $error")
}
}
// 複数ファイルアップロード
def uploadMultipleFiles(): Unit = {
val document = new File("/path/to/document.pdf")
val image = new File("/path/to/image.jpg")
val data = new File("/path/to/data.json")
val response = basicRequest
.post(uri"https://api.example.com/upload/batch")
.multipartBody(
multipart("title", "一括アップロード"),
multipart("description", "複数ファイルの一括処理"),
multipart("document", document).fileName("doc.pdf"),
multipart("image", image).fileName("image.jpg"),
multipart("data", data).fileName("data.json")
)
.send(backend)
println(s"一括アップロード結果: ${response.body}")
}
// ファイルダウンロード
def downloadFile(): Unit = {
val downloadPath = "/path/to/downloads/downloaded_file.pdf"
val response = basicRequest
.get(uri"https://api.example.com/files/document.pdf")
.response(asFile(new File(downloadPath)))
.send(backend)
response.body match {
case Right(file) =>
println(s"ダウンロード完了: ${file.getAbsolutePath}")
println(s"ファイルサイズ: ${file.length()} bytes")
case Left(error) =>
println(s"ダウンロード失敗: $error")
}
}
// ストリーミングダウンロード(大容量ファイル対応)
def streamingDownload(): Unit = {
val response = basicRequest
.get(uri"https://api.example.com/large-file.zip")
.response(asStream[InputStream])
.send(backend)
response.body match {
case Right(inputStream) =>
val outputPath = Paths.get("/path/to/downloads/large-file.zip")
Files.copy(inputStream, outputPath)
inputStream.close()
println(s"ストリーミングダウンロード完了: $outputPath")
case Left(error) =>
println(s"ストリーミングダウンロード失敗: $error")
}
}
// バイナリデータの送受信
def binaryDataHandling(): Unit = {
val binaryData: Array[Byte] = Files.readAllBytes(Paths.get("/path/to/binary.dat"))
// バイナリデータ送信
val uploadResponse = basicRequest
.post(uri"https://api.example.com/binary")
.body(binaryData)
.header("Content-Type", "application/octet-stream")
.send(backend)
// バイナリデータ受信
val downloadResponse = basicRequest
.get(uri"https://api.example.com/binary/12345")
.response(asByteArray)
.send(backend)
downloadResponse.body match {
case Right(bytes) =>
Files.write(Paths.get("/path/to/received_binary.dat"), bytes)
println(s"バイナリデータ受信完了: ${bytes.length} bytes")
case Left(error) =>
println(s"バイナリデータ受信失敗: $error")
}
}
// WebSocket接続例
def webSocketExample(): Unit = {
import sttp.client4.httpclient.HttpClientSyncBackend
import sttp.ws.WebSocket
val wsBackend = HttpClientSyncBackend()
val response = basicRequest
.get(uri"wss://echo.websocket.org")
.response(asWebSocket { ws: WebSocket[Identity] =>
// メッセージ送信
ws.sendText("Hello WebSocket!")
// メッセージ受信
val received = ws.receiveText()
println(s"受信メッセージ: $received")
// WebSocket終了
ws.close()
received.getOrElse("")
})
.send(wsBackend)
response.body match {
case Right(result) => println(s"WebSocket通信結果: $result")
case Left(error) => println(s"WebSocket接続失敗: $error")
}
wsBackend.close()
}
}
パフォーマンス最適化と監視
import sttp.client4.*
import sttp.client4.logging.LoggingBackend
import sttp.client4.wrappers.FollowRedirectsBackend
import scala.concurrent.duration.*
object PerformanceOptimizationExamples {
// 最適化されたバックエンド設定
def createOptimizedBackend(): SttpBackend[Identity, Any] = {
val baseBackend = DefaultSyncBackend(
options = SttpBackendOptions(
connectionTimeout = 30.seconds,
proxy = None
)
)
// ログ機能付きバックエンド
val loggingBackend = LoggingBackend(
delegate = baseBackend,
logRequestBody = true,
logResponseBody = true
)
// リダイレクト対応
FollowRedirectsBackend(loggingBackend)
}
// 接続プール最適化
def connectionPoolOptimization(): Unit = {
import java.net.http.HttpClient
import java.time.Duration
val httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(30))
.version(HttpClient.Version.HTTP_2) // HTTP/2優先
.followRedirects(HttpClient.Redirect.NORMAL)
.build()
val backend = HttpClientSyncBackend.usingClient(httpClient)
// 設定確認のためのテストリクエスト
val response = basicRequest
.get(uri"https://httpbin.org/get")
.send(backend)
println(s"HTTP version: ${response.httpVersion}")
println(s"Response time: ${response.responseTime}")
backend.close()
}
// 並行リクエスト処理
def concurrentRequests(): Unit = {
import scala.concurrent.{Future, ExecutionContext}
import scala.concurrent.duration.*
import scala.util.{Success, Failure}
implicit val ec: ExecutionContext = ExecutionContext.global
val backend = DefaultFutureBackend()
val userIds = (1 to 20).toList
// 並行でユーザー情報取得
val futures = userIds.map { id =>
basicRequest
.get(uri"https://api.example.com/users/$id")
.response(asJson[User])
.send(backend)
.map { response =>
(id, response.body)
}
}
// 全レスポンス待機
val startTime = System.currentTimeMillis()
Future.sequence(futures).onComplete {
case Success(results) =>
val endTime = System.currentTimeMillis()
val successCount = results.count(_._2.isRight)
println(s"並行処理完了: ${endTime - startTime}ms")
println(s"成功: $successCount / ${results.length}")
backend.close()
case Failure(exception) =>
println(s"並行処理失敗: ${exception.getMessage}")
backend.close()
}
}
// リクエスト監視とメトリクス
def requestMonitoring(): Unit = {
var requestCount = 0
var totalResponseTime = 0L
val monitoringBackend = new SttpBackend[Identity, Any] {
def send[T](request: Request[T, Any]): Response[T] = {
val startTime = System.currentTimeMillis()
requestCount += 1
println(s"Request #$requestCount: ${request.method} ${request.uri}")
val response = DefaultSyncBackend().send(request)
val responseTime = System.currentTimeMillis() - startTime
totalResponseTime += responseTime
println(s"Response #$requestCount: ${response.code} (${responseTime}ms)")
println(s"Average response time: ${totalResponseTime / requestCount}ms")
response
}
def close(): Unit = DefaultSyncBackend().close()
def responseMonad: MonadError[Identity] = IdMonad
}
// 監視機能付きリクエスト実行
val response1 = basicRequest.get(uri"https://httpbin.org/get").send(monitoringBackend)
val response2 = basicRequest.get(uri"https://httpbin.org/status/200").send(monitoringBackend)
val response3 = basicRequest.get(uri"https://httpbin.org/delay/1").send(monitoringBackend)
println(s"Total requests: $requestCount")
println(s"Average response time: ${totalResponseTime / requestCount}ms")
monitoringBackend.close()
}
// キャッシュ機能実装
class CachingBackend(delegate: SttpBackend[Identity, Any]) extends SttpBackend[Identity, Any] {
private val cache = scala.collection.mutable.Map[String, (Response[String], Long)]()
private val cacheTimeout = 60000 // 1分
def send[T](request: Request[T, Any]): Response[T] = {
val cacheKey = s"${request.method}:${request.uri}"
val currentTime = System.currentTimeMillis()
// GETリクエストのみキャッシュ対象
if (request.method.method == "GET") {
cache.get(cacheKey) match {
case Some((cachedResponse, timestamp)) if currentTime - timestamp < cacheTimeout =>
println(s"Cache hit: $cacheKey")
cachedResponse.asInstanceOf[Response[T]]
case _ =>
println(s"Cache miss: $cacheKey")
val response = delegate.send(request)
if (response.code.code == 200) {
cache.put(cacheKey, (response.asInstanceOf[Response[String]], currentTime))
}
response
}
} else {
delegate.send(request)
}
}
def close(): Unit = {
cache.clear()
delegate.close()
}
def responseMonad: MonadError[Identity] = delegate.responseMonad
}
def cachingExample(): Unit = {
val cachingBackend = new CachingBackend(DefaultSyncBackend())
// 同じリクエストを複数回実行
val url = uri"https://httpbin.org/get"
println("1回目のリクエスト:")
val response1 = basicRequest.get(url).send(cachingBackend)
println("2回目のリクエスト(キャッシュから):")
val response2 = basicRequest.get(url).send(cachingBackend)
println("3回目のリクエスト(キャッシュから):")
val response3 = basicRequest.get(url).send(cachingBackend)
cachingBackend.close()
}
}
実用的な使用パターン
API統合サービス構築
import sttp.client4.*
import zio.*
import io.circe.*
import io.circe.generic.semiauto.*
// 実際のAPIサービス統合例
case class GitHubRepository(
id: Long,
name: String,
full_name: String,
description: Option[String],
stargazers_count: Int,
language: Option[String]
)
case class SearchResult(
total_count: Int,
items: List[GitHubRepository]
)
implicit val repoDecoder: Decoder[GitHubRepository] = deriveDecoder[GitHubRepository]
implicit val searchDecoder: Decoder[SearchResult] = deriveDecoder[SearchResult]
class GitHubApiService(backend: SttpBackend[Task, Any]) {
private val baseUrl = uri"https://api.github.com"
def searchRepositories(query: String, sort: String = "stars", per_page: Int = 30): Task[SearchResult] = {
basicRequest
.get(uri"$baseUrl/search/repositories?q=$query&sort=$sort&per_page=$per_page")
.header("Accept", "application/vnd.github.v3+json")
.header("User-Agent", "sttp-example/1.0")
.response(asJson[SearchResult])
.send(backend)
.flatMap { response =>
response.body match {
case Right(result) => ZIO.succeed(result)
case Left(error) => ZIO.fail(new RuntimeException(s"GitHub API error: $error"))
}
}
}
def getRepository(owner: String, repo: String): Task[GitHubRepository] = {
basicRequest
.get(uri"$baseUrl/repos/$owner/$repo")
.response(asJson[GitHubRepository])
.send(backend)
.flatMap { response =>
response.body match {
case Right(repository) => ZIO.succeed(repository)
case Left(error) => ZIO.fail(new RuntimeException(s"Repository not found: $error"))
}
}
}
}
// 使用例
object GitHubServiceExample extends ZIOAppDefault {
def run = {
val program = for {
backend <- ZIO.service[SttpBackend[Task, Any]]
githubService = new GitHubApiService(backend)
// Scalaプロジェクト検索
scalaRepos <- githubService.searchRepositories("language:scala", "stars", 10)
_ <- ZIO.log(s"Found ${scalaRepos.total_count} Scala repositories")
topRepos = scalaRepos.items.take(5)
_ <- ZIO.foreach(topRepos) { repo =>
ZIO.log(s"${repo.name}: ${repo.stargazers_count} stars")
}
// 特定リポジトリの詳細取得
sttpRepo <- githubService.getRepository("softwaremill", "sttp")
_ <- ZIO.log(s"sttp repository: ${sttpRepo.description.getOrElse("No description")}")
} yield ()
program.provide(
HttpClientZioBackend.layer()
)
}
}