sttp

Scala向けの統一HTTPクライアントライブラリ。複数のバックエンド(Java HttpClient、Akka HTTP、http4s、OkHttp等)に対応した共通インターフェース提供。JSON処理(circe、uPickle等)、ストリーミング(fs2、ZIO Streams等)、関数型プログラミングライブラリとシームレス統合。

HTTPクライアントScala関数型型安全マルチプラットフォーム非同期

GitHub概要

softwaremill/sttp

The Scala HTTP client you always wanted!

スター1,481
ウォッチ37
フォーク322
作成日:2017年6月30日
言語:Scala
ライセンス:Apache License 2.0

トピックス

akka-httpasynchronouscatsclientdistributed-tracinghttphttp-clienthttpclientinterpolatormonixokhttpreactive-streamsscalascalazsynchronousurizipkinzipkin-brave

スター履歴

softwaremill/sttp Star History
データ取得日時: 2025/7/18 01:39

ライブラリ

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()
    )
  }
}