You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用TypeLevel Scala构建HTTP服务时的http4s编译错误排查

解决TypeLevel Scala调用NWS API的编译错误

下面针对你遇到的三个编译错误逐一给出解决方案:


1. URI插值器歧义冲突

错误原因

http4s和sttp两个库都提供了uri字符串插值器,编译器无法确定使用哪一个,导致隐式转换歧义。

解决方案

显式指定使用sttp的URI插值器,避免冲突:

// 替换原uri调用为sttp的显式引用
.get(sttp.client3.UriContext.uri"https://api.weather.gov/points/$latitude,$longitude")

或者在导入时排除http4s的插值器(如果不需要使用http4s的uri语法):

// 不导入http4s的LiteralsSyntax,避免和sttp冲突
import org.http4s._
import org.http4s.dsl.io._
import org.http4s.implicits._
import org.http4s.blaze.server.BlazeServerBuilder
import org.http4s.circe._

2. Circe缺少Map[String, Any]的Decoder

错误原因

Circe默认不提供Map[String, Any]的解码器,这类非类型安全的解析方式不符合Circe的设计理念,更推荐用case class解析结构化JSON。

解决方案

定义对应NWS API响应结构的case class,利用circe-generic自动生成类型安全的解码器:

// 新增对应NWS API响应的case class
case class NwsPointsResponse(properties: PointsProperties)
case class PointsProperties(forecast: String)

case class NwsForecastResponse(properties: ForecastProperties)
case class ForecastProperties(periods: List[ForecastPeriod])
case class ForecastPeriod(shortForecast: String, temperature: Double)

修改getWeather方法的响应解析逻辑:

def getWeather(latitude: Double, longitude: Double): IO[WeatherResponse] = {
  val backend = HttpURLConnectionBackend()
  val request = basicRequest
    .get(sttp.client3.UriContext.uri"https://api.weather.gov/points/$latitude,$longitude")
    .response(asJson[NwsPointsResponse])

  IO.fromEither(request.send(backend).body)
    .flatMap { pointsRes =>
      val forecastUrl = pointsRes.properties.forecast
      val forecastRequest = basicRequest
        .get(sttp.client3.UriContext.uri"$forecastUrl")
        .response(asJson[NwsForecastResponse])

      IO.fromEither(forecastRequest.send(backend).body)
        .map { forecastRes =>
          val todayPeriod = forecastRes.properties.periods.head
          WeatherResponse(todayPeriod.shortForecast, classifyTemperature(todayPeriod.temperature))
        }
    }
}

3. WeatherResponse缺少EntityEncoder

错误原因

http4s需要将WeatherResponse序列化为HTTP响应实体,但默认没有对应的EntityEncoder,需要将Circe的Encoder转换为http4s的EntityEncoder。

解决方案

导入http4s-circe的EntityEncoder扩展:

// 在原导入基础上添加这一行
import org.http4s.circe.CirceEntityEncoder._

修改后的完整代码

package weather

import cats.effect._
import org.http4s._
import org.http4s.dsl.io._
import org.http4s.implicits._
import org.http4s.blaze.server.BlazeServerBuilder
import org.http4s.circe._
import org.http4s.circe.CirceEntityEncoder._
import io.circe.generic.auto._
import sttp.client3._
import sttp.client3.circe._

object WeatherServer extends IOApp {

  case class WeatherResponse(forecast: String, temperatureType: String)

  // NWS API响应对应的case class
  case class NwsPointsResponse(properties: PointsProperties)
  case class PointsProperties(forecast: String)
  case class NwsForecastResponse(properties: ForecastProperties)
  case class ForecastProperties(periods: List[ForecastPeriod])
  case class ForecastPeriod(shortForecast: String, temperature: Double)

  def classifyTemperature(temp: Double): String = {
    if (temp < 10) "cold"
    else if (temp > 25) "hot"
    else "moderate"
  }

  def getWeather(latitude: Double, longitude: Double): IO[WeatherResponse] = {
    val backend = HttpURLConnectionBackend()
    val request = basicRequest
      .get(sttp.client3.UriContext.uri"https://api.weather.gov/points/$latitude,$longitude")
      .response(asJson[NwsPointsResponse])

    IO.fromEither(request.send(backend).body)
      .flatMap { pointsRes =>
        val forecastUrl = pointsRes.properties.forecast
        val forecastRequest = basicRequest
          .get(sttp.client3.UriContext.uri"$forecastUrl")
          .response(asJson[NwsForecastResponse])

        IO.fromEither(forecastRequest.send(backend).body)
          .map { forecastRes =>
            val todayPeriod = forecastRes.properties.periods.head
            WeatherResponse(todayPeriod.shortForecast, classifyTemperature(todayPeriod.temperature))
          }
      }
  }

  val weatherService = HttpRoutes.of[IO] {
    case GET -> Root / "weather" :? LatitudeQueryParamMatcher(lat) +& LongitudeQueryParamMatcher(lon) =>
      getWeather(lat, lon).flatMap { weather =>
        Ok(weather)
      }
  }

  val httpApp = weatherService.orNotFound

  def run(args: List[String]): IO[ExitCode] =
    BlazeServerBuilder[IO](runtime.compute)
      .bindHttp(8080, "0.0.0.0")
      .withHttpApp(httpApp)
      .serve
      .compile
      .drain
      .as(ExitCode.Success)
}

object LatitudeQueryParamMatcher extends QueryParamDecoderMatcher[Double]("latitude")
object LongitudeQueryParamMatcher extends QueryParamDecoderMatcher[Double]("longitude")

内容的提问来源于stack exchange,提问作者PacificNW_Lover

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 02:25:57