├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build.sbt ├── http-verbs-play-30 └── src │ ├── main │ ├── resources │ │ └── reference.conf │ └── scala │ │ └── uk │ │ └── gov │ │ └── hmrc │ │ ├── http │ │ ├── CollectionUtils.scala │ │ ├── HeaderCarrier.scala │ │ ├── HeaderNames.scala │ │ ├── HttpClient.scala │ │ ├── HttpDelete.scala │ │ ├── HttpErrorFunctions.scala │ │ ├── HttpExceptions.scala │ │ ├── HttpGet.scala │ │ ├── HttpPatch.scala │ │ ├── HttpPost.scala │ │ ├── HttpPut.scala │ │ ├── HttpReads.scala │ │ ├── HttpReadsInstances.scala │ │ ├── HttpReadsLegacyInstances.scala │ │ ├── HttpResponse.scala │ │ ├── HttpTransport.scala │ │ ├── HttpVerb.scala │ │ ├── HttpVerbs.scala │ │ ├── JsValidationException.scala │ │ ├── Request.scala │ │ ├── Retries.scala │ │ ├── TypeUtil.scala │ │ ├── UrlValidationException.scala │ │ ├── client │ │ │ ├── HttpClientV2.scala │ │ │ ├── HttpClientV2Impl.scala │ │ │ └── package.scala │ │ ├── controllers │ │ │ └── JsPathEnrichment.scala │ │ ├── hooks │ │ │ ├── HttpHook.scala │ │ │ └── HttpHooks.scala │ │ ├── logging │ │ │ ├── ConnectionTracing.scala │ │ │ └── LoggingDetails.scala │ │ └── package.scala │ │ └── play │ │ └── http │ │ ├── BodyCaptor.scala │ │ ├── HeaderCarrierConverter.scala │ │ ├── logging │ │ ├── Mdc.scala │ │ └── MdcLoggingExecutionContext.scala │ │ └── ws │ │ ├── WSExecute.scala │ │ ├── WSHttpResponse.scala │ │ ├── WSRequest.scala │ │ ├── WSRequestBuilder.scala │ │ ├── default │ │ ├── WSDelete.scala │ │ ├── WSGet.scala │ │ ├── WSPatch.scala │ │ ├── WSPost.scala │ │ └── WSPut.scala │ │ └── verbs.scala │ └── test │ ├── resources │ └── logback.xml │ └── scala │ └── uk │ └── gov │ └── hmrc │ ├── http │ ├── CommonHttpBehaviour.scala │ ├── HeaderCarrierSpec.scala │ ├── HeadersSpec.scala │ ├── HttpDeleteSpec.scala │ ├── HttpErrorFunctionsSpec.scala │ ├── HttpGetSpec.scala │ ├── HttpPatchSpec.scala │ ├── HttpPostSpec.scala │ ├── HttpPutSpec.scala │ ├── HttpReadsInstancesSpec.scala │ ├── HttpReadsLegacyInstancesSpec.scala │ ├── HttpResponseSpec.scala │ ├── RetriesSpec.scala │ ├── client │ │ └── HttpClientV2Spec.scala │ ├── controllers │ │ └── JsPathEnrichmentSpec.scala │ └── logging │ │ └── ConnectionTracingSpec.scala │ └── play │ ├── http │ ├── HeaderCarrierConverterSpec.scala │ └── logging │ │ ├── MdcLoggingExecutionContextSpec.scala │ │ └── MdcSpec.scala │ └── test │ ├── PortFinder.scala │ └── WireMockSupport.scala ├── http-verbs-test-play-30 └── src │ ├── main │ └── scala │ │ └── uk │ │ └── gov │ │ └── hmrc │ │ ├── http │ │ └── test │ │ │ ├── ExternalWireMockSupport.scala │ │ │ ├── HttpClientSupport.scala │ │ │ ├── HttpClientV2Support.scala │ │ │ ├── PortFinder.scala │ │ │ └── WireMockSupport.scala │ │ └── play │ │ └── http │ │ └── test │ │ └── ResponseMatchers.scala │ └── test │ ├── resources │ ├── __files │ │ ├── bankHolidays.json │ │ ├── bankHolidays.xml │ │ └── userId.json │ └── logback.xml │ └── scala │ └── uk │ └── gov │ └── hmrc │ └── http │ ├── examples │ └── Examples.scala │ └── test │ └── SupportSpec.scala ├── project ├── CopySources.scala ├── LibDependencies.scala ├── build.properties └── plugins.sbt └── repository.yaml /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | target 3 | logs 4 | lib_managed 5 | pids 6 | 7 | */*dependency-reduced-pom.xml 8 | 9 | *.jar 10 | *.war 11 | *.ear 12 | 13 | *.ipr 14 | *.iws 15 | *.iml 16 | **/*.iml 17 | .idea 18 | 19 | *.DS_Store 20 | **/*.DS_Store 21 | 22 | **/target/* 23 | target 24 | 25 | **/bin/* 26 | **/lib/* 27 | *.classpath 28 | *.project 29 | **/*.settings 30 | .project 31 | .settings 32 | .classpath 33 | .sass-cache 34 | .bloop/ 35 | .metals/ 36 | .vscode/ 37 | metals.sbt 38 | .bsp/ 39 | 40 | nohup.out 41 | rebel.xml 42 | .history 43 | **.orig 44 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import sbt.Keys.crossScalaVersions 2 | import sbt._ 3 | 4 | // Disable multiple project tests running at the same time 5 | // https://www.scala-sbt.org/1.x/docs/Parallel-Execution.html 6 | Global / concurrentRestrictions += Tags.limitSum(1, Tags.Test, Tags.Untagged) 7 | 8 | val scala2_13 = "2.13.16" 9 | val scala3 = "3.3.5" 10 | 11 | ThisBuild / majorVersion := 15 12 | ThisBuild / scalaVersion := scala2_13 13 | ThisBuild / isPublicArtefact := true 14 | ThisBuild / scalacOptions ++= Seq("-feature") 15 | 16 | 17 | lazy val library = (project in file(".")) 18 | .settings( 19 | publish / skip := true, 20 | crossScalaVersions := Seq.empty 21 | ) 22 | .aggregate( 23 | httpVerbsPlay29, httpVerbsTestPlay29, 24 | httpVerbsPlay30, httpVerbsTestPlay30 25 | ) 26 | .disablePlugins(sbt.plugins.JUnitXmlReportPlugin) 27 | 28 | def copyPlay30Sources(module: Project) = 29 | CopySources.copySources( 30 | module, 31 | transformSource = _.replace("org.apache.pekko", "akka"), 32 | transformResource = _.replace("pekko", "akka") 33 | ) 34 | 35 | lazy val httpVerbsPlay29 = Project("http-verbs-play-29", file("http-verbs-play-29")) 36 | .enablePlugins(BuildInfoPlugin) 37 | .settings( 38 | copyPlay30Sources(httpVerbsPlay30), 39 | crossScalaVersions := Seq(scala2_13), 40 | libraryDependencies ++= 41 | LibDependencies.coreCompileCommon(scalaVersion.value) ++ 42 | LibDependencies.coreCompilePlay29 ++ 43 | LibDependencies.coreTestCommon ++ 44 | LibDependencies.coreTestPlay29, 45 | Test / fork := true // akka is not unloaded properly, which can affect other tests 46 | ) 47 | .settings( // https://github.com/sbt/sbt-buildinfo 48 | buildInfoKeys := Seq[BuildInfoKey](version), 49 | buildInfoPackage := "uk.gov.hmrc.http" 50 | ) 51 | 52 | lazy val httpVerbsPlay30 = Project("http-verbs-play-30", file("http-verbs-play-30")) 53 | .enablePlugins(BuildInfoPlugin) 54 | .settings( 55 | crossScalaVersions := Seq(scala2_13, scala3), 56 | libraryDependencies ++= 57 | LibDependencies.coreCompileCommon(scalaVersion.value) ++ 58 | LibDependencies.coreCompilePlay30 ++ 59 | LibDependencies.coreTestCommon ++ 60 | LibDependencies.coreTestPlay30, 61 | Test / fork := true // pekko is not unloaded properly, which can affect other tests 62 | ) 63 | .settings( // https://github.com/sbt/sbt-buildinfo 64 | buildInfoKeys := Seq[BuildInfoKey](version), 65 | buildInfoPackage := "uk.gov.hmrc.http" 66 | ) 67 | 68 | lazy val httpVerbsTestPlay29 = Project("http-verbs-test-play-29", file("http-verbs-test-play-29")) 69 | .settings( 70 | copyPlay30Sources(httpVerbsTestPlay30), 71 | crossScalaVersions := Seq(scala2_13), 72 | libraryDependencies ++= LibDependencies.testCompilePlay29, 73 | Test / fork := true // required to look up wiremock resources 74 | ) 75 | .dependsOn(httpVerbsPlay29) 76 | 77 | lazy val httpVerbsTestPlay30 = Project("http-verbs-test-play-30", file("http-verbs-test-play-30")) 78 | .settings( 79 | crossScalaVersions := Seq(scala2_13, scala3), 80 | libraryDependencies ++= LibDependencies.testCompilePlay30, 81 | Test / fork := true // required to look up wiremock resources 82 | ) 83 | .dependsOn(httpVerbsPlay30) 84 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/resources/reference.conf: -------------------------------------------------------------------------------- 1 | # Copyright 2023 HM Revenue & Customs 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # http://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | internalServiceHostPatterns = [ "^.*\\.service$", "^.*\\.mdtp$", "^localhost$" ] 16 | bootstrap.http.headersAllowlist = [] 17 | http-verbs { 18 | retries { 19 | intervals = [ "500.millis", "1.second", "2.seconds", "4.seconds", "8.seconds" ] 20 | ssl-engine-closed-already.enabled = false 21 | } 22 | 23 | auditing.maxBodyLength = 32665 24 | 25 | proxy.enabled = false 26 | } 27 | 28 | # the following need providing if http-verbs.proxy.enabled is set to true 29 | proxy.protocol = null 30 | proxy.host = null 31 | proxy.port = null 32 | proxy.username = null 33 | proxy.password = null 34 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/CollectionUtils.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | object CollectionUtils { 20 | // play returns scala.collection.Seq, but default for Scala 2.13 is scala.collection.immutable.Seq 21 | private [http] def forScala2_13(m: Map[String, scala.collection.Seq[String]]): Map[String, Seq[String]] = 22 | // `m.mapValues(_.toSeq).toMap` by itself strips the ordering away 23 | scala.collection.immutable.TreeMap[String, Seq[String]]()(scala.math.Ordering.comparatorToOrdering(String.CASE_INSENSITIVE_ORDER)) ++ m.view.mapValues(_.toSeq) 24 | 25 | } 26 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/HeaderCarrier.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | import java.net.URL 20 | 21 | import org.slf4j.{Logger, LoggerFactory} 22 | import uk.gov.hmrc.http.logging.LoggingDetails 23 | 24 | import scala.util.matching.Regex 25 | 26 | case class HeaderCarrier( 27 | authorization : Option[Authorization] = None, 28 | forwarded : Option[ForwardedFor] = None, 29 | sessionId : Option[SessionId] = None, 30 | requestId : Option[RequestId] = None, 31 | requestChain : RequestChain = RequestChain.init, 32 | nsStamp : Long = System.nanoTime(), 33 | extraHeaders : Seq[(String, String)] = Seq(), 34 | trueClientIp : Option[String] = None, 35 | trueClientPort : Option[String] = None, 36 | gaToken : Option[String] = None, 37 | gaUserId : Option[String] = None, 38 | deviceID : Option[String] = None, 39 | akamaiReputation: Option[AkamaiReputation] = None, 40 | otherHeaders : Seq[(String, String)] = Seq() 41 | ) extends LoggingDetails { 42 | private val logger: Logger = LoggerFactory.getLogger(getClass) 43 | 44 | /** 45 | * @return the time, in nanoseconds, since this header carrier was created 46 | */ 47 | def age: Long = System.nanoTime() - nsStamp 48 | 49 | val names: HeaderNames.type = HeaderNames 50 | 51 | private lazy val explicitHeaders: Seq[(String, String)] = 52 | Seq( 53 | HeaderNames.xRequestId -> requestId.map(_.value), 54 | HeaderNames.xSessionId -> sessionId.map(_.value), 55 | HeaderNames.xForwardedFor -> forwarded.map(_.value), 56 | HeaderNames.xRequestChain -> Some(requestChain.value), 57 | HeaderNames.authorisation -> authorization.map(_.value), 58 | HeaderNames.trueClientIp -> trueClientIp, 59 | HeaderNames.trueClientPort -> trueClientPort, 60 | HeaderNames.googleAnalyticTokenId -> gaToken, 61 | HeaderNames.googleAnalyticUserId -> gaUserId, 62 | HeaderNames.deviceID -> deviceID, 63 | HeaderNames.akamaiReputation -> akamaiReputation.map(_.value) 64 | ).collect { case (k, Some(v)) => (k, v) } 65 | 66 | def withExtraHeaders(headers: (String, String)*): HeaderCarrier = 67 | this.copy(extraHeaders = extraHeaders ++ headers) 68 | 69 | def headers(names: Seq[String]): Seq[(String, String)] = { 70 | val namesLc = names.map(_.toLowerCase) 71 | (explicitHeaders ++ otherHeaders).filter { case (k, _) => namesLc.contains(k.toLowerCase) } 72 | } 73 | 74 | def headersForUrl(config: HeaderCarrier.Config)(url: String): Seq[(String, String)] = { 75 | val isInternalHost = config.internalHostPatterns.exists(_.pattern.matcher(new URL(url).getHost).matches()) 76 | 77 | val hdrs = 78 | (if (isInternalHost) 79 | headers(HeaderNames.explicitlyIncludedHeaders ++ config.headersAllowlist) 80 | else Seq.empty 81 | ) ++ 82 | config.userAgent.map("User-Agent" -> _).toSeq ++ 83 | extraHeaders 84 | 85 | val duplicates = hdrs.groupBy(_._1).collect { case (k, vs) if vs.length > 1 => k } 86 | if (duplicates.nonEmpty) 87 | logger.warn(s"The following headers were detected multiple times: ${duplicates.mkString(",")}") 88 | 89 | hdrs 90 | } 91 | } 92 | 93 | object HeaderCarrier { 94 | 95 | def headersForUrl( 96 | config : Config, 97 | url : String, 98 | extraHeaders: Seq[(String, String)] = Seq() 99 | )( 100 | implicit hc: HeaderCarrier 101 | ): Seq[(String, String)] = 102 | hc.withExtraHeaders(extraHeaders: _*).headersForUrl(config)(url) 103 | 104 | case class Config( 105 | internalHostPatterns : Seq[Regex] = Seq.empty, 106 | headersAllowlist : Seq[String] = Seq.empty, 107 | userAgent : Option[String] = None 108 | ) 109 | 110 | object Config { 111 | private val logger: Logger = LoggerFactory.getLogger(getClass) 112 | 113 | def fromConfig(config: com.typesafe.config.Config): Config = { 114 | import scala.jdk.CollectionConverters._ 115 | 116 | if (config.hasPath("httpHeadersWhitelist")) 117 | logger.warn("Use of configuration key 'httpHeadersWhitelist' will be IGNORED. Use 'bootstrap.http.headersAllowlist' instead") 118 | 119 | Config( 120 | internalHostPatterns = config.getStringList("internalServiceHostPatterns" ).asScala.toSeq.map(_.r), 121 | headersAllowlist = config.getStringList("bootstrap.http.headersAllowlist" ).asScala.toSeq, 122 | userAgent = if (config.hasPath("appName")) Some(config.getString("appName")) else None 123 | ) 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/HeaderNames.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | object HeaderNames { 20 | 21 | /* 22 | * this isn't ideal, but downstream apps still want to refer to typed header values 23 | * and guarantee their explicit allowlisting whilst "remaining headers" should avoid 24 | * duplicating these and creating unnecessary data on the wire. 25 | * We could just model as a list but then accessing known header names would 26 | * have to be done by magic number and would be susceptible to changes in ordering 27 | */ 28 | 29 | val authorisation = "Authorization" 30 | val xForwardedFor = "x-forwarded-for" 31 | val xRequestId = "X-Request-ID" 32 | val xRequestTimestamp = "X-Request-Timestamp" 33 | val xSessionId = "X-Session-ID" 34 | val xRequestChain = "X-Request-Chain" 35 | val trueClientIp = "True-Client-IP" 36 | val trueClientPort = "True-Client-Port" 37 | val surrogate = "Surrogate" 38 | val otacAuthorization = "Otac-Authorization" 39 | val googleAnalyticTokenId = "ga-token" 40 | val googleAnalyticUserId = "ga-user-cookie-id" 41 | val deviceID = "deviceID" // not a typo, should be ID 42 | val akamaiReputation = "Akamai-Reputation" 43 | 44 | val explicitlyIncludedHeaders = Seq( 45 | authorisation, 46 | xForwardedFor, 47 | xRequestId, 48 | xRequestTimestamp, 49 | xSessionId, 50 | xRequestChain, 51 | trueClientIp, 52 | trueClientPort, 53 | surrogate, 54 | otacAuthorization, 55 | googleAnalyticTokenId, 56 | googleAnalyticUserId, 57 | deviceID, // not a typo, should be ID 58 | akamaiReputation 59 | ) 60 | } 61 | 62 | object CookieNames { 63 | val deviceID = "mdtpdi" 64 | } 65 | 66 | object SessionKeys { 67 | val sessionId = "sessionId" 68 | val authToken = "authToken" 69 | val otacToken = "otacToken" 70 | val lastRequestTimestamp = "ts" 71 | val redirect = "login_redirect" 72 | val npsVersion = "nps-version" 73 | val sensitiveUserId = "suppressUserIs" 74 | val postLogoutPage = "postLogoutPage" 75 | val loginOrigin = "loginOrigin" 76 | val portalRedirectUrl = "portalRedirectUrl" 77 | val portalState = "portalState" 78 | } 79 | 80 | case class Authorization(value: String) extends AnyVal 81 | 82 | case class SessionId(value: String) extends AnyVal 83 | 84 | case class RequestId(value: String) extends AnyVal 85 | 86 | case class AkamaiReputation(value: String) extends AnyVal 87 | 88 | case class RequestChain(value: String) extends AnyVal { 89 | def extend = RequestChain(s"$value-${RequestChain.newComponent}") 90 | } 91 | 92 | object RequestChain { 93 | def newComponent = (scala.util.Random.nextInt() & 0xffff).toHexString 94 | 95 | def init = RequestChain(newComponent) 96 | } 97 | 98 | case class ForwardedFor(value: String) extends AnyVal 99 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/HttpClient.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | @deprecated("Use HttpClientV2", "15.0.0") 20 | trait HttpClient extends HttpGet with HttpPut with HttpPost with HttpDelete with HttpPatch 21 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/HttpDelete.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | import uk.gov.hmrc.http.HttpVerbs.{DELETE => DELETE_VERB} 20 | import uk.gov.hmrc.http.hooks.{HttpHooks, RequestData, ResponseData} 21 | import uk.gov.hmrc.http.logging.ConnectionTracing 22 | 23 | import scala.concurrent.{ExecutionContext, Future} 24 | 25 | @deprecated("Use HttpClientV2", "15.0.0") 26 | trait HttpDelete 27 | extends CoreDelete 28 | with DeleteHttpTransport 29 | with HttpVerb 30 | with ConnectionTracing 31 | with HttpHooks 32 | with Retries { 33 | 34 | private lazy val hcConfig = HeaderCarrier.Config.fromConfig(configuration) 35 | 36 | override def DELETE[O]( 37 | url : String, 38 | headers: Seq[(String, String)] = Seq.empty 39 | )(implicit 40 | rds: HttpReads[O], 41 | hc : HeaderCarrier, 42 | ec : ExecutionContext 43 | ): Future[O] = 44 | withTracing(DELETE_VERB, url) { 45 | val allHeaders = HeaderCarrier.headersForUrl(hcConfig, url, headers) :+ "Http-Client-Version" -> BuildInfo.version 46 | val httpResponse = retryOnSslEngineClosed(DELETE_VERB, url)(doDelete(url, allHeaders)) 47 | executeHooks( 48 | DELETE_VERB, 49 | url"$url", 50 | RequestData(allHeaders, None), 51 | httpResponse.map(ResponseData.fromHttpResponse) 52 | ) 53 | mapErrors(DELETE_VERB, url, httpResponse).map(rds.read(DELETE_VERB, url, _)) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/HttpErrorFunctions.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | import org.apache.pekko.stream.Materializer 20 | 21 | import scala.concurrent.{Await, TimeoutException} 22 | import scala.concurrent.duration.{Duration, DurationInt} 23 | 24 | trait HttpErrorFunctions { 25 | def notFoundMessage(verbName: String, url: String, responseBody: String): String = 26 | s"$verbName of '$url' returned 404 (Not Found). Response body: '$responseBody'" 27 | 28 | def preconditionFailedMessage(verbName: String, url: String, responseBody: String): String = 29 | s"$verbName of '$url' returned 412 (Precondition Failed). Response body: '$responseBody'" 30 | 31 | def upstreamResponseMessage(verbName: String, url: String, status: Int, responseBody: String): String = 32 | s"$verbName of '$url' returned $status. Response body: '$responseBody'" 33 | 34 | def badRequestMessage(verbName: String, url: String, responseBody: String): String = 35 | s"$verbName of '$url' returned 400 (Bad Request). Response body '$responseBody'" 36 | 37 | def badGatewayMessage(verbName: String, url: String, status: Int, responseBody: String): String = 38 | s"$verbName of '$url' returned status $status. Response body: '$responseBody'" 39 | 40 | def badGatewayMessage(verbName: String, url: String, e: Exception): String = 41 | s"$verbName of '$url' failed. Caused by: '${e.getMessage}'" 42 | 43 | def gatewayTimeoutMessage(verbName: String, url: String, e: Exception): String = 44 | s"$verbName of '$url' timed out with message '${e.getMessage}'" 45 | 46 | def is2xx(status: Int) = status >= 200 && status < 300 47 | 48 | def is4xx(status: Int) = status >= 400 && status < 500 49 | 50 | def is5xx(status: Int) = status >= 500 && status < 600 51 | 52 | // Note, no special handling of BadRequest or NotFound 53 | // they will be returned as `Left(Upstream4xxResponse(status = 400))` and `Left(Upstream4xxResponse(status = 404))` respectively 54 | def handleResponseEither(httpMethod: String, url: String)(response: HttpResponse): Either[UpstreamErrorResponse, HttpResponse] = 55 | response.status match { 56 | case status if is4xx(status) || is5xx(status) => 57 | Left(UpstreamErrorResponse( 58 | message = upstreamResponseMessage(httpMethod, url, status, response.body), 59 | statusCode = status, 60 | reportAs = if (is4xx(status)) HttpExceptions.INTERNAL_SERVER_ERROR else HttpExceptions.BAD_GATEWAY, 61 | headers = response.headers 62 | )) 63 | // Note all cases not handled above (e.g. 1xx, 2xx and 3xx) will be returned as is 64 | // default followRedirect should mean we don't see 3xx... 65 | case status => Right(response) 66 | } 67 | 68 | /* Same as `handleResponseEither` but should be used when reading the `HttpResponse` as a stream. 69 | * The error is returned as `Source[UpstreamErrorResponse, _]`. 70 | */ 71 | def handleResponseEitherStream( 72 | httpMethod: String, 73 | url : String 74 | )( 75 | response: HttpResponse 76 | )(implicit 77 | mat : Materializer, 78 | errorTimeout: ErrorTimeout 79 | ): Either[UpstreamErrorResponse, HttpResponse] = 80 | response.status match { 81 | case status if is4xx(status) || is5xx(status) => 82 | Left { 83 | val errorMessageF = 84 | response.bodyAsSource.runFold("")(_ + _.utf8String) 85 | val errorMessage = 86 | // this await is unfortunate, but HttpReads doesn't support Future 87 | try { 88 | Await.result(errorMessageF, errorTimeout.toDuration) 89 | } catch { 90 | case e: TimeoutException => "" 91 | } 92 | UpstreamErrorResponse( 93 | message = upstreamResponseMessage(httpMethod, url, status, errorMessage), 94 | statusCode = status, 95 | reportAs = if (is4xx(status)) HttpExceptions.INTERNAL_SERVER_ERROR else HttpExceptions.BAD_GATEWAY, 96 | headers = response.headers 97 | ) 98 | } 99 | // Note all cases not handled above (e.g. 1xx, 2xx and 3xx) will be returned as is 100 | // default followRedirect should mean we don't see 3xx... 101 | case status => Right(response) 102 | } 103 | } 104 | 105 | object HttpErrorFunctions extends HttpErrorFunctions 106 | 107 | case class ErrorTimeout( 108 | toDuration: Duration 109 | ) extends AnyVal 110 | 111 | object ErrorTimeout { 112 | implicit val errorTimeout: ErrorTimeout = 113 | ErrorTimeout(10.seconds) 114 | } 115 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/HttpExceptions.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | import uk.gov.hmrc.http.HttpExceptions._ 20 | 21 | private object HttpExceptions { 22 | 23 | val BAD_REQUEST = 400 24 | val UNAUTHORIZED = 401 25 | val PAYMENT_REQUIRED = 402 26 | val FORBIDDEN = 403 27 | val NOT_FOUND = 404 28 | val METHOD_NOT_ALLOWED = 405 29 | val NOT_ACCEPTABLE = 406 30 | val PROXY_AUTHENTICATION_REQUIRED = 407 31 | val REQUEST_TIMEOUT = 408 32 | val CONFLICT = 409 33 | val GONE = 410 34 | val LENGTH_REQUIRED = 411 35 | val PRECONDITION_FAILED = 412 36 | val REQUEST_ENTITY_TOO_LARGE = 413 37 | val REQUEST_URI_TOO_LONG = 414 38 | val UNSUPPORTED_MEDIA_TYPE = 415 39 | val REQUESTED_RANGE_NOT_SATISFIABLE = 416 40 | val EXPECTATION_FAILED = 417 41 | val UNPROCESSABLE_ENTITY = 422 42 | val LOCKED = 423 43 | val FAILED_DEPENDENCY = 424 44 | val TOO_MANY_REQUEST = 429 45 | 46 | val INTERNAL_SERVER_ERROR = 500 47 | val NOT_IMPLEMENTED = 501 48 | val BAD_GATEWAY = 502 49 | val SERVICE_UNAVAILABLE = 503 50 | val GATEWAY_TIMEOUT = 504 51 | val HTTP_VERSION_NOT_SUPPORTED = 505 52 | val INSUFFICIENT_STORAGE = 507 53 | } 54 | 55 | /** Represents an error occuring within the service itself. 56 | * See [[UpstreamErrorResponse]] for errors returned from Upstream services. 57 | */ 58 | class HttpException(val message: String, val responseCode: Int) extends Exception(message) 59 | 60 | //400s 61 | class BadRequestException(message: String) extends HttpException(message, BAD_REQUEST) 62 | 63 | class UnauthorizedException(message: String) extends HttpException(message, UNAUTHORIZED) 64 | 65 | class PaymentRequiredException(message: String) extends HttpException(message, PAYMENT_REQUIRED) 66 | 67 | class ForbiddenException(message: String) extends HttpException(message, FORBIDDEN) 68 | 69 | class NotFoundException(message: String) extends HttpException(message, NOT_FOUND) 70 | 71 | class MethodNotAllowedException(message: String) extends HttpException(message, METHOD_NOT_ALLOWED) 72 | 73 | class NotAcceptableException(message: String) extends HttpException(message, NOT_ACCEPTABLE) 74 | 75 | class ProxyAuthenticationRequiredException(message: String) 76 | extends HttpException(message, PROXY_AUTHENTICATION_REQUIRED) 77 | 78 | class RequestTimeoutException(message: String) extends HttpException(message, REQUEST_TIMEOUT) 79 | 80 | class ConflictException(message: String) extends HttpException(message, CONFLICT) 81 | 82 | class GoneException(message: String) extends HttpException(message, GONE) 83 | 84 | class LengthRequiredException(message: String) extends HttpException(message, LENGTH_REQUIRED) 85 | 86 | class PreconditionFailedException(message: String) extends HttpException(message, PRECONDITION_FAILED) 87 | 88 | class RequestEntityTooLargeException(message: String) extends HttpException(message, REQUEST_ENTITY_TOO_LARGE) 89 | 90 | class RequestUriTooLongException(message: String) extends HttpException(message, REQUEST_URI_TOO_LONG) 91 | 92 | class UnsupportedMediaTypeException(message: String) extends HttpException(message, UNSUPPORTED_MEDIA_TYPE) 93 | 94 | class RequestRangeNotSatisfiableException(message: String) 95 | extends HttpException(message, REQUESTED_RANGE_NOT_SATISFIABLE) 96 | 97 | class ExpectationFailedException(message: String) extends HttpException(message, EXPECTATION_FAILED) 98 | 99 | class UnprocessableEntityException(message: String) extends HttpException(message, UNPROCESSABLE_ENTITY) 100 | 101 | class LockedException(message: String) extends HttpException(message, LOCKED) 102 | 103 | class FailedDependencyException(message: String) extends HttpException(message, FAILED_DEPENDENCY) 104 | 105 | class TooManyRequestException(message: String) extends HttpException(message, TOO_MANY_REQUEST) 106 | 107 | //500s 108 | class InternalServerException(message: String) extends HttpException(message, INTERNAL_SERVER_ERROR) 109 | 110 | class NotImplementedException(message: String) extends HttpException(message, NOT_IMPLEMENTED) 111 | 112 | class BadGatewayException(message: String) extends HttpException(message, BAD_GATEWAY) 113 | 114 | class ServiceUnavailableException(message: String) extends HttpException(message, SERVICE_UNAVAILABLE) 115 | 116 | class GatewayTimeoutException(message: String) extends HttpException(message, GATEWAY_TIMEOUT) 117 | 118 | class HttpVersionNotSupportedException(message: String) extends HttpException(message, HTTP_VERSION_NOT_SUPPORTED) 119 | 120 | class InsufficientStorageException(message: String) extends HttpException(message, INSUFFICIENT_STORAGE) 121 | 122 | 123 | /** Represent unhandled status codes returned from upstream */ 124 | // The concrete instances are deprecated, so we can eventually just replace with a case class. 125 | // They should be created via UpstreamErrorResponse.apply and deconstructed via the UpstreamErrorResponse.unapply functions 126 | case class UpstreamErrorResponse( 127 | message : String, 128 | statusCode: Int, 129 | reportAs : Int, 130 | headers : Map[String, Seq[String]] 131 | ) extends Exception(message) 132 | 133 | object UpstreamErrorResponse { 134 | def apply(message: String, statusCode: Int): UpstreamErrorResponse = 135 | apply( 136 | message = message, 137 | statusCode = statusCode, 138 | reportAs = statusCode, 139 | headers = Map.empty 140 | ) 141 | 142 | def apply(message: String, statusCode: Int, reportAs: Int): UpstreamErrorResponse = 143 | apply( 144 | message = message, 145 | statusCode = statusCode, 146 | reportAs = reportAs, 147 | headers = Map.empty 148 | ) 149 | 150 | object Upstream4xxResponse { 151 | def unapply(e: UpstreamErrorResponse): Option[UpstreamErrorResponse] = 152 | if (e.statusCode >= 400 && e.statusCode < 500) Some(e) else None 153 | } 154 | 155 | object Upstream5xxResponse { 156 | def unapply(e: UpstreamErrorResponse): Option[UpstreamErrorResponse] = 157 | if (e.statusCode >= 500 && e.statusCode < 600) Some(e) else None 158 | } 159 | 160 | object WithStatusCode { 161 | def unapply(e: UpstreamErrorResponse): Option[Int] = 162 | Some(e.statusCode) 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/HttpGet.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | import java.net.URLEncoder 20 | 21 | import uk.gov.hmrc.http.HttpVerbs.{GET => GET_VERB} 22 | import uk.gov.hmrc.http.hooks.{HttpHooks, RequestData, ResponseData} 23 | import uk.gov.hmrc.http.logging.ConnectionTracing 24 | 25 | import scala.concurrent.{ExecutionContext, Future} 26 | 27 | @deprecated("Use HttpClientV2", "15.0.0") 28 | trait HttpGet 29 | extends CoreGet 30 | with GetHttpTransport 31 | with HttpVerb 32 | with ConnectionTracing 33 | with HttpHooks 34 | with Retries { 35 | 36 | private lazy val hcConfig = HeaderCarrier.Config.fromConfig(configuration) 37 | 38 | override def GET[A]( 39 | url : String, 40 | queryParams: Seq[(String, String)], 41 | headers : Seq[(String, String)] 42 | )(implicit 43 | rds: HttpReads[A], 44 | hc : HeaderCarrier, 45 | ec : ExecutionContext 46 | ): Future[A] = { 47 | if (queryParams.nonEmpty && url.contains("?")) { 48 | throw new UrlValidationException( 49 | url, 50 | s"${this.getClass}.GET(url, queryParams)", 51 | "Query parameters should be provided in either url or as a Seq of tuples") 52 | } 53 | 54 | val urlWithQuery = url + makeQueryString(queryParams) 55 | 56 | withTracing(GET_VERB, urlWithQuery) { 57 | val allHeaders = HeaderCarrier.headersForUrl(hcConfig, url, headers) :+ "Http-Client-Version" -> BuildInfo.version 58 | val httpResponse = retryOnSslEngineClosed(GET_VERB, urlWithQuery)(doGet(urlWithQuery, headers = allHeaders)) 59 | executeHooks( 60 | GET_VERB, 61 | url"$url", 62 | RequestData(allHeaders, None), 63 | httpResponse.map(ResponseData.fromHttpResponse) 64 | ) 65 | mapErrors(GET_VERB, urlWithQuery, httpResponse).map(response => rds.read(GET_VERB, urlWithQuery, response)) 66 | } 67 | } 68 | 69 | private def makeQueryString(queryParams: Seq[(String, String)]) = { 70 | val paramPairs = queryParams.map { case (k, v) => s"$k=${URLEncoder.encode(v, "utf-8")}" } 71 | if (paramPairs.isEmpty) "" else paramPairs.mkString("?", "&", "") 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/HttpPatch.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | import play.api.libs.json.{Json, Writes} 20 | import uk.gov.hmrc.http.HttpVerbs.{PATCH => PATCH_VERB} 21 | import uk.gov.hmrc.http.hooks.{Data, HookData, HttpHooks, RequestData, ResponseData} 22 | import uk.gov.hmrc.http.logging.ConnectionTracing 23 | 24 | import scala.concurrent.{ExecutionContext, Future} 25 | 26 | @deprecated("Use HttpClientV2", "15.0.0") 27 | trait HttpPatch 28 | extends CorePatch 29 | with PatchHttpTransport 30 | with HttpVerb 31 | with ConnectionTracing 32 | with HttpHooks 33 | with Retries { 34 | 35 | private lazy val hcConfig = HeaderCarrier.Config.fromConfig(configuration) 36 | 37 | override def PATCH[I, O]( 38 | url : String, 39 | body : I, 40 | headers: Seq[(String, String)] 41 | )(implicit 42 | wts: Writes[I], 43 | rds: HttpReads[O], 44 | hc : HeaderCarrier, 45 | ec : ExecutionContext 46 | ): Future[O] = 47 | withTracing(PATCH_VERB, url) { 48 | val allHeaders = HeaderCarrier.headersForUrl(hcConfig, url, headers) :+ "Http-Client-Version" -> BuildInfo.version 49 | val httpResponse = retryOnSslEngineClosed(PATCH_VERB, url)(doPatch(url, body, allHeaders)) 50 | executeHooks( 51 | PATCH_VERB, 52 | url"$url", 53 | RequestData(allHeaders, Some(Data.pure(HookData.FromString(Json.stringify(wts.writes(body)))))), 54 | httpResponse.map(ResponseData.fromHttpResponse) 55 | ) 56 | mapErrors(PATCH_VERB, url, httpResponse).map(response => rds.read(PATCH_VERB, url, response)) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/HttpPost.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | import play.api.libs.json.{Json, Writes} 20 | import uk.gov.hmrc.http.HttpVerbs.{POST => POST_VERB} 21 | import uk.gov.hmrc.http.hooks.{Data, HookData, HttpHooks, RequestData, ResponseData} 22 | import uk.gov.hmrc.http.logging.ConnectionTracing 23 | 24 | import scala.concurrent.{ExecutionContext, Future} 25 | 26 | @deprecated("Use HttpClientV2", "15.0.0") 27 | trait HttpPost 28 | extends CorePost 29 | with PostHttpTransport 30 | with HttpVerb 31 | with ConnectionTracing 32 | with HttpHooks 33 | with Retries { 34 | 35 | private lazy val hcConfig = HeaderCarrier.Config.fromConfig(configuration) 36 | 37 | override def POST[I, O]( 38 | url : String, 39 | body : I, 40 | headers: Seq[(String, String)] 41 | )(implicit 42 | wts: Writes[I], 43 | rds: HttpReads[O], 44 | hc : HeaderCarrier, 45 | ec : ExecutionContext 46 | ): Future[O] = 47 | withTracing(POST_VERB, url) { 48 | val allHeaders = HeaderCarrier.headersForUrl(hcConfig, url, headers) :+ "Http-Client-Version" -> BuildInfo.version 49 | val httpResponse = retryOnSslEngineClosed(POST_VERB, url)(doPost(url, body, allHeaders)) 50 | executeHooks( 51 | POST_VERB, 52 | url"$url", 53 | RequestData(allHeaders, Some(Data.pure(HookData.FromString(Json.stringify(wts.writes(body)))))), 54 | httpResponse.map(ResponseData.fromHttpResponse) 55 | ) 56 | mapErrors(POST_VERB, url, httpResponse).map(rds.read(POST_VERB, url, _)) 57 | } 58 | 59 | override def POSTString[O]( 60 | url : String, 61 | body : String, 62 | headers: Seq[(String, String)] 63 | )(implicit 64 | rds: HttpReads[O], 65 | hc : HeaderCarrier, 66 | ec : ExecutionContext 67 | ): Future[O] = 68 | withTracing(POST_VERB, url) { 69 | val allHeaders = HeaderCarrier.headersForUrl(hcConfig, url, headers) :+ "Http-Client-Version" -> BuildInfo.version 70 | val httpResponse = retryOnSslEngineClosed(POST_VERB, url)(doPostString(url, body, allHeaders)) 71 | executeHooks( 72 | POST_VERB, 73 | url"$url", 74 | RequestData(allHeaders, Some(Data.pure(HookData.FromString(body)))), 75 | httpResponse.map(ResponseData.fromHttpResponse) 76 | ) 77 | mapErrors(POST_VERB, url, httpResponse).map(rds.read(POST_VERB, url, _)) 78 | } 79 | 80 | override def POSTForm[O]( 81 | url : String, 82 | body : Map[String, Seq[String]], 83 | headers: Seq[(String, String)] 84 | )(implicit 85 | rds: HttpReads[O], 86 | hc : HeaderCarrier, 87 | ec : ExecutionContext 88 | ): Future[O] = 89 | withTracing(POST_VERB, url) { 90 | val allHeaders = HeaderCarrier.headersForUrl(hcConfig, url, headers) :+ "Http-Client-Version" -> BuildInfo.version 91 | val httpResponse = retryOnSslEngineClosed(POST_VERB, url)(doFormPost(url, body, allHeaders)) 92 | executeHooks( 93 | POST_VERB, 94 | url"$url", 95 | RequestData(allHeaders, Some(Data.pure(HookData.FromMap(body)))), 96 | httpResponse.map(ResponseData.fromHttpResponse) 97 | ) 98 | mapErrors(POST_VERB, url, httpResponse).map(rds.read(POST_VERB, url, _)) 99 | } 100 | 101 | override def POSTEmpty[O]( 102 | url : String, 103 | headers: Seq[(String, String)] 104 | )(implicit 105 | rds: HttpReads[O], 106 | hc : HeaderCarrier, 107 | ec : ExecutionContext 108 | ): Future[O] = 109 | withTracing(POST_VERB, url) { 110 | val allHeaders = HeaderCarrier.headersForUrl(hcConfig, url, headers) :+ "Http-Client-Version" -> BuildInfo.version 111 | val httpResponse = retryOnSslEngineClosed(POST_VERB, url)(doEmptyPost(url, allHeaders)) 112 | executeHooks( 113 | POST_VERB, 114 | url"$url", 115 | RequestData(allHeaders, None), 116 | httpResponse.map(ResponseData.fromHttpResponse) 117 | ) 118 | mapErrors(POST_VERB, url, httpResponse).map(rds.read(POST_VERB, url, _)) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/HttpPut.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | import play.api.libs.json.{Json, Writes} 20 | import uk.gov.hmrc.http.HttpVerbs.{PUT => PUT_VERB} 21 | import uk.gov.hmrc.http.hooks.{Data, HookData, HttpHooks, RequestData, ResponseData} 22 | import uk.gov.hmrc.http.logging.ConnectionTracing 23 | 24 | import scala.concurrent.{ExecutionContext, Future} 25 | 26 | @deprecated("Use HttpClientV2", "15.0.0") 27 | trait HttpPut 28 | extends CorePut 29 | with PutHttpTransport 30 | with HttpVerb 31 | with ConnectionTracing 32 | with HttpHooks 33 | with Retries { 34 | 35 | private lazy val hcConfig = HeaderCarrier.Config.fromConfig(configuration) 36 | 37 | override def PUT[I, O]( 38 | url : String, 39 | body : I, 40 | headers: Seq[(String, String)] 41 | )(implicit 42 | wts: Writes[I], 43 | rds: HttpReads[O], 44 | hc : HeaderCarrier, 45 | ec : ExecutionContext 46 | ): Future[O] = 47 | withTracing(PUT_VERB, url) { 48 | val allHeaders = HeaderCarrier.headersForUrl(hcConfig, url, headers) :+ "Http-Client-Version" -> BuildInfo.version 49 | val httpResponse = retryOnSslEngineClosed(PUT_VERB, url)(doPut(url, body, allHeaders)) 50 | executeHooks( 51 | PUT_VERB, 52 | url"$url", 53 | RequestData(allHeaders, Some(Data.pure(HookData.FromString(Json.stringify(wts.writes(body)))))), 54 | httpResponse.map(ResponseData.fromHttpResponse) 55 | ) 56 | mapErrors(PUT_VERB, url, httpResponse).map(response => rds.read(PUT_VERB, url, response)) 57 | } 58 | 59 | override def PUTString[O]( 60 | url : String, 61 | body : String, 62 | headers: Seq[(String, String)] 63 | )(implicit 64 | rds: HttpReads[O], 65 | hc: HeaderCarrier, 66 | ec: ExecutionContext 67 | ): Future[O] = 68 | withTracing(PUT_VERB, url) { 69 | val allHeaders = HeaderCarrier.headersForUrl(hcConfig, url, headers) :+ "Http-Client-Version" -> BuildInfo.version 70 | val httpResponse = retryOnSslEngineClosed(PUT_VERB, url)(doPutString(url, body, allHeaders)) 71 | executeHooks( 72 | PUT_VERB, 73 | url"$url", 74 | RequestData(allHeaders, Some(Data.pure(HookData.FromString(body)))), 75 | httpResponse.map(ResponseData.fromHttpResponse) 76 | ) 77 | mapErrors(PUT_VERB, url, httpResponse).map(rds.read(PUT_VERB, url, _)) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/HttpReads.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | object HttpReads extends HttpReadsLegacyInstances { 20 | def apply[A : HttpReads] = 21 | implicitly[HttpReads[A]] 22 | 23 | def pure[A](a: A) = 24 | new HttpReads[A] { 25 | def read(method: String, url: String, response: HttpResponse): A = 26 | a 27 | } 28 | 29 | // i.e. HttpReads[A] = Reader[(Method, Url, HttpResponse), A] 30 | def ask: HttpReads[(String, String, HttpResponse)] = 31 | new HttpReads[(String, String, HttpResponse)] { 32 | def read(method: String, url: String, response: HttpResponse): (String, String, HttpResponse) = 33 | (method, url, response) 34 | } 35 | 36 | 37 | // readRaw is brought in like this rather than in a trait as this gives it 38 | // compilation priority during implicit resolution. This means, unless 39 | // specified otherwise a verb call will return a plain HttpResponse 40 | @deprecated("Use uk.gov.hmrc.http.HttpReads.Implicits instead. See README for differences.", "11.0.0") 41 | implicit val readRaw: HttpReads[HttpResponse] = HttpReadsLegacyRawReads.readRaw 42 | 43 | object Implicits extends HttpReadsInstances 44 | } 45 | 46 | trait HttpReads[A] { 47 | outer => 48 | 49 | def read(method: String, url: String, response: HttpResponse): A 50 | 51 | def map[B](fn: A => B): HttpReads[B] = 52 | flatMap(a => HttpReads.pure(fn(a))) 53 | 54 | def flatMap[B](fn: A => HttpReads[B]): HttpReads[B] = 55 | new HttpReads[B] { 56 | def read(method: String, url: String, response: HttpResponse): B = 57 | fn(outer.read(method, url, response)).read(method, url, response) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/HttpReadsInstances.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | import play.api.libs.json.{JsValue, JsError, JsResult, JsSuccess, Reads => JsonReads} 20 | import scala.reflect.ClassTag 21 | import scala.util.Try 22 | 23 | trait HttpReadsInstances 24 | extends HttpReadsHttpResponse 25 | with HttpReadsEither 26 | with HttpReadsTry 27 | with HttpReadsOption 28 | with HttpReadsJson 29 | with LowPriorityHttpReadsJson 30 | 31 | object HttpReadsInstances extends HttpReadsInstances 32 | 33 | import HttpReadsInstances._ 34 | 35 | trait HttpReadsHttpResponse { 36 | /** returns the HttpResponse as is - you will be responsible for checking any status codes. */ 37 | implicit val readRaw: HttpReads[HttpResponse] = 38 | HttpReads.ask.map { case (_, _, response) => response } 39 | 40 | /** Ignores the response and returns Unit - useful for handling 204 etc. 41 | * It can be combined with error handling types (e.g. `Either[UpstreamErrorResponse, Unit]`) 42 | */ 43 | implicit val readUnit: HttpReads[Unit] = 44 | HttpReads.pure(()) 45 | } 46 | 47 | trait HttpReadsEither { 48 | implicit def readEitherOf[A : HttpReads]: HttpReads[Either[UpstreamErrorResponse, A]] = 49 | HttpReads.ask.flatMap { case (method, url, response) => 50 | HttpErrorFunctions.handleResponseEither(method, url)(response) match { 51 | case Left(err) => HttpReads.pure(Left(err)) 52 | case Right(response) => HttpReads[A].map(Right.apply) 53 | } 54 | } 55 | 56 | def throwOnFailure[A](reads: HttpReads[Either[UpstreamErrorResponse, A]]): HttpReads[A] = 57 | reads 58 | .map { 59 | case Left(err) => throw err 60 | case Right(value) => value 61 | } 62 | } 63 | 64 | trait HttpReadsTry { 65 | implicit def readTryOf[A : HttpReads]: HttpReads[Try[A]] = 66 | new HttpReads[Try[A]] { 67 | def read(method: String, url: String, response: HttpResponse): Try[A] = 68 | Try(HttpReads[A].read(method, url, response)) 69 | } 70 | } 71 | 72 | trait HttpReadsOption { 73 | /** An opinionated HttpReads which returns None for 404. 74 | * This does not have any special treatment for 204, as did the previous version. 75 | * If you need a None for any UpstreamErrorResponse, consider using: 76 | * {{{ 77 | * HttpReads[Either[UpstreamErrorResponse, A]].map(_.toOption) 78 | * }}} 79 | */ 80 | implicit def readOptionOfNotFound[A : HttpReads]: HttpReads[Option[A]] = 81 | HttpReads[HttpResponse] 82 | .flatMap(_.status match { 83 | case 404 => HttpReads.pure(None) 84 | case _ => HttpReads[A].map(Some.apply) // this delegates error handling to HttpReads[A] 85 | }) 86 | } 87 | 88 | trait HttpReadsJson { 89 | implicit val readJsValue: HttpReads[JsValue] = 90 | HttpReads[HttpResponse] 91 | .map(_.json) 92 | 93 | /** Note to read json regardless of error response - can define your own: 94 | * {{{ 95 | * HttpReads[HttpResponse].map(_.json.validate[A]) 96 | * }}} 97 | * or custom behaviour - define your own: 98 | * {{{ 99 | * HttpReads[HttpResponse].map(response => response.status match { 100 | * case 200 => Right(response.body.json.validate[A]) 101 | * case 400 => Right(response.body.json.validate[A]) 102 | * case other => Left(s"Invalid status code: \$other") 103 | * }) 104 | * }}} 105 | */ 106 | implicit def readJsResult[A : JsonReads]: HttpReads[JsResult[A]] = 107 | HttpReads[JsValue] 108 | .map(_.validate[A]) 109 | } 110 | 111 | trait LowPriorityHttpReadsJson { 112 | 113 | /** This is probably the typical instance to use, since all http calls occur within `Future`, allowing recovery. 114 | */ 115 | @throws(classOf[UpstreamErrorResponse]) 116 | @throws(classOf[JsValidationException]) 117 | implicit def readFromJson[A](implicit rds: JsonReads[A], ct: ClassTag[A]): HttpReads[A] = 118 | HttpReads[Either[UpstreamErrorResponse, JsResult[A]]] 119 | .flatMap { 120 | case Left(err) => throw err 121 | case Right(JsError(errors)) => HttpReads.ask.map { case (method, url, response) => 122 | throw new JsValidationException(method, url, ct.runtimeClass, errors.toString) 123 | } 124 | case Right(JsSuccess(value, _)) => HttpReads.pure(value) 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/HttpReadsLegacyInstances.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | import play.api.libs.json.{JsNull, JsValue, Reads} 20 | 21 | import scala.reflect.ClassTag 22 | 23 | 24 | trait HttpReadsLegacyInstances extends HttpReadsLegacyOption with HttpReadsLegacyJson 25 | 26 | private[http] object LegacyHttpErrorFunctions extends HttpErrorFunctions { 27 | def handleResponse(httpMethod: String, url: String)(response: HttpResponse): HttpResponse = 28 | handleResponseEither(httpMethod, url)(response) 29 | .fold(err => 30 | err.statusCode match { 31 | case 400 => throw new BadRequestException(badRequestMessage(httpMethod, url, response.body)) 32 | case 404 => throw new NotFoundException(notFoundMessage(httpMethod, url, response.body)) 33 | case _ => throw err 34 | }, 35 | identity 36 | ) 37 | } 38 | 39 | trait HttpReadsLegacyRawReads { 40 | 41 | @deprecated("Use uk.gov.hmrc.http.HttpReads.Implicits instead. See README for differences.", "11.0.0") 42 | implicit val readRaw: HttpReads[HttpResponse] = 43 | (method: String, url: String, response: HttpResponse) => 44 | LegacyHttpErrorFunctions.handleResponse(method, url)(response) 45 | } 46 | 47 | object HttpReadsLegacyRawReads extends HttpReadsLegacyRawReads 48 | 49 | trait HttpReadsLegacyOption { 50 | @deprecated("Use uk.gov.hmrc.http.HttpReads.Implicits instead. See README for differences.", "11.0.0") 51 | implicit def readOptionOf[P](implicit rds: HttpReads[P]): HttpReads[Option[P]] = 52 | (method: String, url: String, response: HttpResponse) => 53 | response.status match { 54 | case 204 | 404 => None 55 | case _ => Some(rds.read(method, url, response)) 56 | } 57 | } 58 | 59 | trait HttpReadsLegacyJson { 60 | @deprecated("Use uk.gov.hmrc.http.HttpReads.Implicits instead. See README for differences.", "11.0.0") 61 | implicit def readFromJson[O](implicit rds: Reads[O], ct: ClassTag[O]): HttpReads[O] = 62 | (method: String, url: String, response: HttpResponse) => 63 | readJson(method, url, LegacyHttpErrorFunctions.handleResponse(method, url)(response).json) 64 | 65 | @deprecated("Use uk.gov.hmrc.http.HttpReads.Implicits instead. See README for differences.", "11.0.0") 66 | def readSeqFromJsonProperty[O](name: String)(implicit rds: Reads[O], ct: ClassTag[O]): HttpReads[Seq[O]] = 67 | (method: String, url: String, response: HttpResponse) => 68 | response.status match { 69 | case 204 | 404 => Seq.empty 70 | case _ => 71 | readJson[Seq[O]](method, url, (LegacyHttpErrorFunctions.handleResponse(method, url)(response).json \ name).getOrElse(JsNull)) //Added JsNull here to force validate to fail - replicates existing behaviour 72 | } 73 | 74 | private def readJson[A](method: String, url: String, jsValue: JsValue)(implicit rds: Reads[A], ct: ClassTag[A]): A = 75 | jsValue 76 | .validate[A] 77 | .fold( 78 | errs => throw new JsValidationException(method, url, ct.runtimeClass, errs.toString()), 79 | valid => valid 80 | ) 81 | } 82 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/HttpResponse.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | import org.apache.pekko.stream.scaladsl.Source 20 | import org.apache.pekko.util.ByteString 21 | import play.api.libs.json.{JsValue, Json} 22 | 23 | /** 24 | * The ws.Response class is very hard to dummy up as it wraps a concrete instance of 25 | * the ning http Response. This trait exposes just the bits of the response that we 26 | * need in methods that we are passing the response to for processing, making it 27 | * much easier to provide dummy data in our specs. 28 | */ 29 | trait HttpResponse { 30 | def status: Int 31 | 32 | def body: String 33 | 34 | def bodyAsSource: Source[ByteString, _] = 35 | Source.single(ByteString(body)) 36 | 37 | /** Converts the body to a JsValue, if possible. 38 | * Consider using `HttpReads` instead for converting the body to Json and handling failures. 39 | * @throws RuntimeException if the body cannot be converted to JsValue. 40 | */ 41 | def json: JsValue = 42 | Json.parse(body) 43 | 44 | def headers: Map[String, Seq[String]] 45 | 46 | def header(key: String): Option[String] = 47 | headers 48 | .collectFirst { case (k, values) if k.equalsIgnoreCase(key) => values } 49 | .flatMap(_.headOption) 50 | 51 | override def toString: String = 52 | s"HttpResponse status=$status" 53 | } 54 | 55 | object HttpResponse { 56 | def apply( 57 | status : Int, 58 | body : String = "", 59 | headers: Map[String, Seq[String]] = Map.empty 60 | ): HttpResponse = { 61 | val pStatus = status 62 | val pBody = body 63 | val pHeaders = headers 64 | new HttpResponse { 65 | override def status = pStatus 66 | override def body = pBody 67 | override def headers = pHeaders 68 | } 69 | } 70 | 71 | 72 | def apply( 73 | status : Int, 74 | json : JsValue, 75 | headers: Map[String, Seq[String]] 76 | ): HttpResponse = 77 | apply( 78 | status = status, 79 | body = Json.prettyPrint(json), 80 | headers = headers 81 | ) 82 | 83 | def apply( 84 | status : Int, 85 | bodyAsSource: Source[ByteString, _], 86 | headers : Map[String, Seq[String]] 87 | ): HttpResponse = { 88 | val pStatus = status 89 | val pBodyAsSource = bodyAsSource 90 | val pHeaders = headers 91 | new HttpResponse { 92 | override def status = pStatus 93 | override def bodyAsSource = pBodyAsSource 94 | override def headers = pHeaders 95 | override def body = sys.error(s"This is a streamed response, please use `bodyAsSource`") 96 | } 97 | } 98 | 99 | def unapply(that: HttpResponse): Option[(Int, String, Map[String, Seq[String]])] = 100 | Some((that.status, that.body, that.headers)) 101 | } 102 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/HttpTransport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | import java.net.URL 20 | 21 | import play.api.libs.json.Writes 22 | 23 | import scala.concurrent.{ExecutionContext, Future} 24 | 25 | @deprecated("Use HttpClientV2", "15.0.0") 26 | trait GetHttpTransport { 27 | def doGet( 28 | url: String, 29 | headers: Seq[(String, String)] = Seq.empty)( 30 | implicit ec: ExecutionContext): Future[HttpResponse] 31 | } 32 | 33 | @deprecated("Use HttpClientV2", "15.0.0") 34 | trait DeleteHttpTransport { 35 | def doDelete( 36 | url: String, 37 | headers: Seq[(String, String)] = Seq.empty)( 38 | implicit ec: ExecutionContext): Future[HttpResponse] 39 | } 40 | 41 | @deprecated("Use HttpClientV2", "15.0.0") 42 | trait PatchHttpTransport { 43 | def doPatch[A]( 44 | url: String, 45 | body: A, 46 | headers: Seq[(String, String)] = Seq.empty)( 47 | implicit rds: Writes[A], 48 | ec: ExecutionContext): Future[HttpResponse] 49 | } 50 | 51 | @deprecated("Use HttpClientV2", "15.0.0") 52 | trait PutHttpTransport { 53 | def doPut[A]( 54 | url: String, 55 | body: A, 56 | headers: Seq[(String, String)] = Seq.empty)( 57 | implicit rds: Writes[A], 58 | ec: ExecutionContext): Future[HttpResponse] 59 | 60 | def doPutString( 61 | url: String, 62 | body: String, 63 | headers: Seq[(String, String)] = Seq.empty)( 64 | implicit ec: ExecutionContext): Future[HttpResponse] 65 | } 66 | 67 | @deprecated("Use HttpClientV2", "15.0.0") 68 | trait PostHttpTransport { 69 | def doPost[A]( 70 | url: String, 71 | body: A, 72 | headers: Seq[(String, String)] = Seq.empty)( 73 | implicit wts: Writes[A], 74 | ec: ExecutionContext): Future[HttpResponse] 75 | 76 | def doPostString( 77 | url: String, 78 | body: String, 79 | headers: Seq[(String, String)] = Seq.empty)( 80 | implicit ec: ExecutionContext): Future[HttpResponse] 81 | 82 | def doEmptyPost[A]( 83 | url: String, 84 | headers: Seq[(String, String)] = Seq.empty)( 85 | implicit ec: ExecutionContext): Future[HttpResponse] 86 | 87 | def doFormPost( 88 | url: String, 89 | body: Map[String, Seq[String]], 90 | headers: Seq[(String, String)] = Seq.empty)( 91 | implicit ec: ExecutionContext): Future[HttpResponse] 92 | } 93 | 94 | @deprecated("Use HttpClientV2", "15.0.0") 95 | trait HttpTransport 96 | extends GetHttpTransport 97 | with DeleteHttpTransport 98 | with PatchHttpTransport 99 | with PutHttpTransport 100 | with PostHttpTransport 101 | 102 | @deprecated("Use HttpClientV2", "15.0.0") 103 | trait CoreGet { 104 | 105 | final def GET[A]( 106 | url: URL)( 107 | implicit rds: HttpReads[A], 108 | hc: HeaderCarrier, 109 | ec: ExecutionContext): Future[A] = 110 | GET(url.toString, Seq.empty, Seq.empty) 111 | 112 | def GET[A]( 113 | url: URL, 114 | headers: Seq[(String, String)])( 115 | implicit rds: HttpReads[A], 116 | hc: HeaderCarrier, 117 | ec: ExecutionContext): Future[A] = 118 | GET(url.toString, Seq.empty, headers) 119 | 120 | def GET[A]( 121 | url: String, 122 | queryParams: Seq[(String, String)] = Seq.empty, 123 | headers: Seq[(String, String)] = Seq.empty)( 124 | implicit rds: HttpReads[A], 125 | hc: HeaderCarrier, 126 | ec: ExecutionContext): Future[A] 127 | 128 | } 129 | 130 | @deprecated("Use HttpClientV2", "15.0.0") 131 | trait CoreDelete { 132 | 133 | final def DELETE[O]( 134 | url: URL)( 135 | implicit rds: HttpReads[O], 136 | hc: HeaderCarrier, 137 | ec: ExecutionContext): Future[O] = 138 | DELETE(url.toString, Seq.empty) 139 | 140 | def DELETE[O]( 141 | url: URL, 142 | headers: Seq[(String, String)])( 143 | implicit rds: HttpReads[O], 144 | hc: HeaderCarrier, 145 | ec: ExecutionContext): Future[O] = 146 | DELETE(url.toString, headers) 147 | 148 | def DELETE[O]( 149 | url: String, 150 | headers: Seq[(String, String)] = Seq.empty)( 151 | implicit rds: HttpReads[O], 152 | hc: HeaderCarrier, 153 | ec: ExecutionContext): Future[O] 154 | } 155 | 156 | @deprecated("Use HttpClientV2", "15.0.0") 157 | trait CorePatch { 158 | 159 | final def PATCH[I, O]( 160 | url: URL, 161 | body: I)( 162 | implicit wts: Writes[I], 163 | rds: HttpReads[O], 164 | hc: HeaderCarrier, 165 | ec: ExecutionContext): Future[O] = 166 | PATCH(url.toString, body, Seq.empty) 167 | 168 | def PATCH[I, O]( 169 | url: URL, 170 | body: I, 171 | headers: Seq[(String, String)])( 172 | implicit wts: Writes[I], 173 | rds: HttpReads[O], 174 | hc: HeaderCarrier, 175 | ec: ExecutionContext): Future[O] = 176 | PATCH(url.toString, body, headers) 177 | 178 | def PATCH[I, O]( 179 | url: String, 180 | body: I, 181 | headers: Seq[(String, String)] = Seq.empty)( 182 | implicit wts: Writes[I], 183 | rds: HttpReads[O], 184 | hc: HeaderCarrier, 185 | ec: ExecutionContext): Future[O] 186 | } 187 | 188 | @deprecated("Use HttpClientV2", "15.0.0") 189 | trait CorePut { 190 | 191 | final def PUT[I, O]( 192 | url: URL, 193 | body: I)( 194 | implicit wts: Writes[I], 195 | rds: HttpReads[O], 196 | hc: HeaderCarrier, 197 | ec: ExecutionContext): Future[O] = 198 | PUT(url.toString, body, Seq.empty) 199 | 200 | def PUT[I, O]( 201 | url: URL, 202 | body: I, 203 | headers: Seq[(String, String)])( 204 | implicit wts: Writes[I], 205 | rds: HttpReads[O], 206 | hc: HeaderCarrier, 207 | ec: ExecutionContext): Future[O] = 208 | PUT(url.toString, body, headers) 209 | 210 | def PUT[I, O]( 211 | url: String, 212 | body: I, 213 | headers: Seq[(String, String)] = Seq.empty)( 214 | implicit wts: Writes[I], 215 | rds: HttpReads[O], 216 | hc: HeaderCarrier, 217 | ec: ExecutionContext): Future[O] 218 | 219 | final def PUTString[O]( 220 | url: URL, 221 | body: String)( 222 | implicit rds: HttpReads[O], 223 | hc: HeaderCarrier, 224 | ec: ExecutionContext): Future[O] = 225 | PUTString(url.toString, body, Seq.empty) 226 | 227 | def PUTString[O]( 228 | url: URL, 229 | body: String, 230 | headers: Seq[(String, String)])( 231 | implicit rds: HttpReads[O], 232 | hc: HeaderCarrier, 233 | ec: ExecutionContext): Future[O] = 234 | PUTString(url.toString, body, headers) 235 | 236 | def PUTString[O]( 237 | url: String, 238 | body: String, 239 | headers: Seq[(String, String)] = Seq.empty)( 240 | implicit rds: HttpReads[O], 241 | hc: HeaderCarrier, 242 | ec: ExecutionContext): Future[O] 243 | } 244 | 245 | @deprecated("Use HttpClientV2", "15.0.0") 246 | trait CorePost { 247 | 248 | final def POST[I, O]( 249 | url: URL, 250 | body: I)( 251 | implicit wts: Writes[I], 252 | rds: HttpReads[O], 253 | hc: HeaderCarrier, 254 | ec: ExecutionContext): Future[O] = 255 | POST(url.toString, body, Seq.empty) 256 | 257 | def POST[I, O]( 258 | url: URL, 259 | body: I, 260 | headers: Seq[(String, String)])( 261 | implicit wts: Writes[I], 262 | rds: HttpReads[O], 263 | hc: HeaderCarrier, 264 | ec: ExecutionContext): Future[O] = 265 | POST(url.toString, body, headers) 266 | 267 | def POST[I, O]( 268 | url: String, 269 | body: I, 270 | headers: Seq[(String, String)] = Seq.empty)( 271 | implicit wts: Writes[I], 272 | rds: HttpReads[O], 273 | hc: HeaderCarrier, 274 | ec: ExecutionContext): Future[O] 275 | 276 | final def POSTString[O]( 277 | url: URL, 278 | body: String)( 279 | implicit rds: HttpReads[O], 280 | hc: HeaderCarrier, 281 | ec: ExecutionContext): Future[O] = 282 | POSTString(url.toString, body, Seq.empty) 283 | 284 | def POSTString[O]( 285 | url: URL, 286 | body: String, 287 | headers: Seq[(String, String)])( 288 | implicit rds: HttpReads[O], 289 | hc: HeaderCarrier, 290 | ec: ExecutionContext): Future[O] = 291 | POSTString(url.toString, body, headers) 292 | 293 | def POSTString[O]( 294 | url: String, 295 | body: String, 296 | headers: Seq[(String, String)] = Seq.empty)( 297 | implicit rds: HttpReads[O], 298 | hc: HeaderCarrier, 299 | ec: ExecutionContext): Future[O] 300 | 301 | final def POSTForm[O]( 302 | url: URL, 303 | body: Map[String, Seq[String]])( 304 | implicit rds: HttpReads[O], 305 | hc: HeaderCarrier, 306 | ec: ExecutionContext): Future[O] = 307 | POSTForm(url.toString, body, Seq.empty) 308 | 309 | def POSTForm[O]( 310 | url: URL, 311 | body: Map[String, Seq[String]], 312 | headers: Seq[(String, String)])( 313 | implicit rds: HttpReads[O], 314 | hc: HeaderCarrier, 315 | ec: ExecutionContext): Future[O] = 316 | POSTForm(url.toString, body, headers) 317 | 318 | def POSTForm[O]( 319 | url: String, 320 | body: Map[String, Seq[String]], 321 | headers: Seq[(String, String)] = Seq.empty)( 322 | implicit rds: HttpReads[O], 323 | hc: HeaderCarrier, 324 | ec: ExecutionContext): Future[O] 325 | 326 | final def POSTEmpty[O]( 327 | url: URL)( 328 | implicit rds: HttpReads[O], 329 | hc: HeaderCarrier, 330 | ec: ExecutionContext): Future[O] = 331 | POSTEmpty(url.toString, Seq.empty) 332 | 333 | def POSTEmpty[O]( 334 | url: URL, 335 | headers: Seq[(String, String)])( 336 | implicit rds: HttpReads[O], 337 | hc: HeaderCarrier, 338 | ec: ExecutionContext): Future[O] = 339 | POSTEmpty(url.toString, headers) 340 | 341 | def POSTEmpty[O]( 342 | url: String, 343 | headers: Seq[(String, String)] = Seq.empty)( 344 | implicit rds: HttpReads[O], 345 | hc: HeaderCarrier, 346 | ec: ExecutionContext): Future[O] 347 | } 348 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/HttpVerb.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | import java.net.ConnectException 20 | import java.util.concurrent.TimeoutException 21 | 22 | import scala.concurrent.{ExecutionContext, Future} 23 | 24 | @deprecated("Use HttpClientV2", "15.0.0") 25 | trait HttpVerb extends Request { 26 | 27 | def mapErrors( 28 | httpMethod: String, 29 | url : String, 30 | f : Future[HttpResponse] 31 | )(implicit 32 | ec: ExecutionContext 33 | ): Future[HttpResponse] = 34 | f.recoverWith { 35 | case e: TimeoutException => Future.failed(new GatewayTimeoutException(gatewayTimeoutMessage(httpMethod, url, e))) 36 | case e: ConnectException => Future.failed(new BadGatewayException(badGatewayMessage(httpMethod, url, e))) 37 | } 38 | 39 | def badGatewayMessage(verbName: String, url: String, e: Exception): String = 40 | s"$verbName of '$url' failed. Caused by: '${e.getMessage}'" 41 | 42 | def gatewayTimeoutMessage(verbName: String, url: String, e: Exception): String = 43 | s"$verbName of '$url' timed out with message '${e.getMessage}'" 44 | } 45 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/HttpVerbs.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | /** 20 | * Standard HTTP Verbs 21 | */ 22 | trait HttpVerbs { 23 | val GET = "GET" 24 | val POST = "POST" 25 | val PUT = "PUT" 26 | val PATCH = "PATCH" 27 | val DELETE = "DELETE" 28 | val HEAD = "HEAD" 29 | val OPTIONS = "OPTIONS" 30 | } 31 | 32 | object HttpVerbs extends HttpVerbs 33 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/JsValidationException.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | class JsValidationException( 20 | val method : String, 21 | val url : String, 22 | val readingAs: Class[_], 23 | val errors : String 24 | ) extends Exception { 25 | 26 | override def getMessage: String = 27 | s"$method of '$url' returned invalid json. Attempting to convert to ${readingAs.getName} gave errors: $errors" 28 | } 29 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/Request.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | @deprecated("Use HttpClientV2", "15.0.0") 20 | trait Request 21 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/Retries.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | import com.typesafe.config.Config 20 | import org.apache.pekko.actor.ActorSystem 21 | import org.apache.pekko.pattern.after 22 | import org.slf4j.LoggerFactory 23 | import uk.gov.hmrc.play.http.logging.Mdc 24 | 25 | import java.util.concurrent.TimeUnit 26 | import javax.net.ssl.SSLException 27 | import scala.concurrent.duration._ 28 | import scala.concurrent.{ExecutionContext, Future} 29 | import scala.jdk.CollectionConverters._ 30 | 31 | trait Retries { 32 | 33 | protected def actorSystem: ActorSystem 34 | 35 | protected def configuration: Config 36 | 37 | private val logger = LoggerFactory.getLogger("application") 38 | 39 | private lazy val sslRetryEnabled = 40 | configuration.getBoolean("http-verbs.retries.ssl-engine-closed-already.enabled") 41 | 42 | def retryOnSslEngineClosed[A](verb: String, url: String)(block: => Future[A])(implicit ec: ExecutionContext): Future[A] = 43 | retryFor(s"$verb $url") { case ex: SSLException if ex.getMessage == "SSLEngine closed already" => sslRetryEnabled }(block) 44 | 45 | @deprecated("Use retryOnSslEngineClosed instead", "14.0.0") 46 | def retry[A](verb: String, url: String)(block: => Future[A])(implicit ec: ExecutionContext): Future[A] = 47 | retryOnSslEngineClosed(verb, url)(block) 48 | 49 | def retryFor[A]( 50 | label : String 51 | )(condition: PartialFunction[Exception, Boolean] 52 | )(block : => Future[A] 53 | )(implicit 54 | ec: ExecutionContext 55 | ): Future[A] = { 56 | def loop(remainingIntervals: Seq[FiniteDuration]): Future[A] = { 57 | // scheduling will loose MDC data. Here we explicitly ensure it is available on block. 58 | block 59 | .recoverWith { 60 | case ex: Exception if condition.lift(ex).getOrElse(false) && remainingIntervals.nonEmpty => 61 | val delay = remainingIntervals.head 62 | logger.warn(s"Retrying $label in $delay due to error: ${ex.getMessage}") 63 | val mdcData = Mdc.mdcData 64 | after(delay, actorSystem.scheduler){ 65 | Mdc.putMdc(mdcData) 66 | loop(remainingIntervals.tail) 67 | } 68 | } 69 | } 70 | loop(intervals) 71 | } 72 | 73 | private[http] lazy val intervals: Seq[FiniteDuration] = 74 | configuration.getDurationList("http-verbs.retries.intervals").asScala.toSeq.map { d => 75 | FiniteDuration(d.toMillis, TimeUnit.MILLISECONDS) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/TypeUtil.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | import izumi.reflect.Tag 20 | 21 | object TypeUtil { 22 | object IsMap { 23 | def unapply[B: Tag](b: B): Option[Map[String, Seq[String]]] = 24 | if (Tag[B].tag =:= Tag[Map[String, String]].tag) 25 | Some( 26 | b.asInstanceOf[Map[String, String]] 27 | .map { case (k, v) => k -> Seq(v) } 28 | ) 29 | else if (Tag[B].tag =:= Tag[Map[String, Seq[String]]].tag) 30 | Some( 31 | b.asInstanceOf[Map[String, Seq[String]]] 32 | ) 33 | else 34 | None 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/UrlValidationException.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | @deprecated("Use HttpClientV2", "15.0.0") 20 | class UrlValidationException(val url: String, val context: String, val message: String) extends Exception { 21 | override def getMessage: String = 22 | s"'$url' is invalid for $context. $message" 23 | } 24 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/client/HttpClientV2.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http.client 18 | 19 | import play.api.libs.ws.{BodyWritable, WSRequest} 20 | import uk.gov.hmrc.http.{HeaderCarrier, HttpReads} 21 | import izumi.reflect.Tag 22 | 23 | import java.net.URL 24 | import scala.concurrent.{ExecutionContext, Future} 25 | 26 | /** This client centralises the execution of the request to ensure that the common concerns (e.g. auditing, logging, 27 | * retries) occur, but makes building the request more flexible (by exposing play-ws). 28 | * It also supports streaming. 29 | */ 30 | trait HttpClientV2 { 31 | protected def mkRequestBuilder(url: URL, method: String)(implicit hc: HeaderCarrier): RequestBuilder 32 | 33 | def get(url: URL)(implicit hc: HeaderCarrier): RequestBuilder = 34 | mkRequestBuilder(url, "GET") 35 | 36 | def post(url: URL)(implicit hc: HeaderCarrier): RequestBuilder = 37 | mkRequestBuilder(url, "POST") 38 | 39 | def put(url: URL)(implicit hc: HeaderCarrier): RequestBuilder = 40 | mkRequestBuilder(url, "PUT") 41 | 42 | def delete(url: URL)(implicit hc: HeaderCarrier): RequestBuilder = 43 | mkRequestBuilder(url, "DELETE") 44 | 45 | def patch(url: URL)(implicit hc: HeaderCarrier): RequestBuilder = 46 | mkRequestBuilder(url, "PATCH") 47 | 48 | def head(url: URL)(implicit hc: HeaderCarrier): RequestBuilder = 49 | mkRequestBuilder(url, "HEAD") 50 | 51 | def options(url: URL)(implicit hc: HeaderCarrier): RequestBuilder = 52 | mkRequestBuilder(url, "OPTIONS") 53 | } 54 | 55 | trait RequestBuilder { 56 | def transform(transform: WSRequest => WSRequest): RequestBuilder 57 | 58 | def execute[A: HttpReads](implicit ec: ExecutionContext): Future[A] 59 | 60 | def stream[A: StreamHttpReads](implicit ec: ExecutionContext): Future[A] 61 | 62 | // support functions 63 | 64 | /** Adds the header. If the header has already been defined (e.g. from HeaderCarrier), it will be replaced. 65 | * It does not affect headers not mentioned. 66 | * 67 | * Use `transform(_.addHttpHeaders)` to append header values to existing. 68 | */ 69 | def setHeader(header: (String, String)*): RequestBuilder 70 | 71 | def withProxy: RequestBuilder 72 | 73 | /** `withBody` should be called rather than `transform(_.withBody)`. 74 | * Failure to do so will lead to a runtime exception 75 | */ 76 | def withBody[B : BodyWritable : Tag](body: B)(implicit ec: ExecutionContext): RequestBuilder 77 | } 78 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/client/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | import org.apache.pekko.util.ByteString 20 | import org.apache.pekko.stream.Materializer 21 | import org.apache.pekko.stream.scaladsl.Source 22 | 23 | import scala.annotation.implicitNotFound 24 | 25 | package client { 26 | trait Streaming 27 | } 28 | package object client 29 | extends StreamHttpReadsInstances { 30 | 31 | // ensures strict HttpReads are not passed to stream function, which would lead to stream being read into memory 32 | // (or runtime exceptions since HttpResponse.body with throw exception for streamed responses) 33 | @implicitNotFound("""Could not find an implicit StreamHttpReads[${A}]. 34 | You may be missing an implicit Materializer.""") 35 | type StreamHttpReads[A] = HttpReads[A] with Streaming 36 | } 37 | 38 | trait StreamHttpReadsInstances { 39 | def tag[A](instance: A): A with client.Streaming = 40 | instance.asInstanceOf[A with client.Streaming] 41 | 42 | implicit val readStreamHttpResponse: client.StreamHttpReads[HttpResponse] = 43 | tag[HttpReads[HttpResponse]]( 44 | HttpReads.ask.map { case (_, _, response) => response } 45 | ) 46 | 47 | implicit def readStreamEitherHttpResponse(implicit mat: Materializer, errorTimeout: ErrorTimeout): client.StreamHttpReads[Either[UpstreamErrorResponse, HttpResponse]] = 48 | tag[HttpReads[Either[UpstreamErrorResponse, HttpResponse]]]( 49 | HttpReads.ask.flatMap { case (method, url, response) => 50 | HttpErrorFunctions.handleResponseEitherStream(method, url)(response) match { 51 | case Left(err) => HttpReads.pure(Left(err)) 52 | case Right(response) => HttpReads.pure(Right(response)) 53 | } 54 | } 55 | ) 56 | 57 | implicit def readEitherSource(implicit mat: Materializer, errorTimeout: ErrorTimeout): client.StreamHttpReads[Either[UpstreamErrorResponse, Source[ByteString, _]]] = 58 | tag[HttpReads[Either[UpstreamErrorResponse, Source[ByteString, _]]]]( 59 | readStreamEitherHttpResponse 60 | .map(_.map(_.bodyAsSource)) 61 | ) 62 | 63 | implicit def readSource(implicit mat: Materializer, errorTimeout: ErrorTimeout): client.StreamHttpReads[Source[ByteString, _]] = 64 | tag[HttpReads[Source[ByteString, _]]]( 65 | readEitherSource 66 | .map { 67 | case Left(err) => throw err 68 | case Right(value) => value 69 | } 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/controllers/JsPathEnrichment.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http.controllers 18 | 19 | import play.api.libs.json._ 20 | 21 | @deprecated("Use JsPath.readNullable", "15.0.0") 22 | object JsPathEnrichment { 23 | 24 | implicit class RichJsPath(jsPath: JsPath) { 25 | 26 | // Existed since (the deprecated) JsPath.readOpt would fail if the path did notexist up to the read point. 27 | def tolerantReadNullable[T](implicit r: Reads[T]): Reads[Option[T]] = 28 | JsPath.readNullable 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/hooks/HttpHook.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http.hooks 18 | 19 | import java.net.URL 20 | import uk.gov.hmrc.http.{HeaderCarrier, HttpResponse} 21 | 22 | import scala.concurrent.{ExecutionContext, Future} 23 | 24 | trait HttpHook { 25 | def apply( 26 | verb : String, 27 | url : URL, 28 | request : RequestData, 29 | responseF: Future[ResponseData] 30 | )(implicit 31 | hc: HeaderCarrier, 32 | ec: ExecutionContext 33 | ): Unit 34 | } 35 | 36 | final case class Data[+A]( 37 | value: A, 38 | isTruncated: Boolean, 39 | isRedacted: Boolean 40 | ) { 41 | 42 | def map[B](f: A => B): Data[B] = 43 | flatMap(a => Data.pure(f(a))) 44 | 45 | def map2[B, C](data: Data[B])(f: (A, B) => C): Data[C] = 46 | flatMap(a => data.map(b => f(a, b))) 47 | 48 | def flatMap[B](f: A => Data[B]): Data[B] = { 49 | val dataB = f(value) 50 | Data( 51 | value = dataB.value, 52 | isTruncated = isTruncated || dataB.isTruncated, 53 | isRedacted = isRedacted || dataB.isRedacted 54 | ) 55 | } 56 | } 57 | 58 | object Data { 59 | 60 | def pure[A](value: A): Data[A] = 61 | Data( 62 | value = value, 63 | isTruncated = false, 64 | isRedacted = false 65 | ) 66 | 67 | def truncated[A](value: A): Data[A] = 68 | pure(value).copy(isTruncated = true) 69 | 70 | def redacted[A](value: A): Data[A] = 71 | pure(value).copy(isRedacted = true) 72 | 73 | def traverse[A, B](seq: Seq[A])(f: A => Data[B]): Data[Seq[B]] = 74 | seq.foldLeft(Data.pure(Seq.empty[B]))((acc, x) => acc.map2(f(x))(_ :+ _)) 75 | } 76 | 77 | case class ResponseData( 78 | body : Data[String], 79 | status : Int, 80 | headers: Map[String, Seq[String]] 81 | ) 82 | 83 | object ResponseData { 84 | def fromHttpResponse(httpResponse: HttpResponse) = 85 | ResponseData( 86 | body = Data.pure(httpResponse.body), 87 | status = httpResponse.status, 88 | headers = httpResponse.headers 89 | ) 90 | } 91 | 92 | case class RequestData( 93 | headers: Seq[(String, String)], 94 | body : Option[Data[HookData]] 95 | ) 96 | 97 | sealed trait HookData 98 | object HookData { 99 | case class FromString(s: String) extends HookData 100 | case class FromMap(m: Map[String, Seq[String]]) extends HookData 101 | } 102 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/hooks/HttpHooks.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http.hooks 18 | 19 | import java.net.URL 20 | 21 | import uk.gov.hmrc.http.HeaderCarrier 22 | 23 | import scala.concurrent.{ExecutionContext, Future} 24 | 25 | trait HttpHooks { 26 | val hooks: Seq[HttpHook] 27 | 28 | val NoneRequired = Seq.empty 29 | 30 | protected def executeHooks( 31 | verb : String, 32 | url : URL, 33 | request : RequestData, 34 | responseF: Future[ResponseData] 35 | )(implicit 36 | hc: HeaderCarrier, 37 | ec: ExecutionContext 38 | ): Unit = 39 | hooks.foreach(_.apply(verb, url, request, responseF)) 40 | } 41 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/logging/ConnectionTracing.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http.logging 18 | 19 | import org.slf4j.{Logger, LoggerFactory} 20 | import uk.gov.hmrc.http.{HttpException, UpstreamErrorResponse} 21 | 22 | import scala.concurrent._ 23 | import scala.util.{Failure, Success, Try} 24 | 25 | trait ConnectionTracing { 26 | 27 | lazy val connectionLogger: Logger = LoggerFactory.getLogger("connector") 28 | 29 | def withTracing[T](method: String, uri: String)( 30 | body: => Future[T])(implicit ld: LoggingDetails, ec: ExecutionContext): Future[T] = { 31 | val startAge = ld.age 32 | val f = body 33 | f.onComplete(logResult(ld, method, uri, startAge)) 34 | f 35 | } 36 | 37 | def logResult[A](ld: LoggingDetails, method: String, uri: String, startAge: Long)(result: Try[A]): Unit = 38 | result match { 39 | case Success(_) => connectionLogger.debug(formatMessage(ld, method, uri, startAge, "ok")) 40 | case Failure(ex: HttpException) if ex.responseCode == 404 => 41 | connectionLogger.info(formatMessage(ld, method, uri, startAge, s"failed ${ex.getMessage}")) 42 | case Failure(ex: UpstreamErrorResponse) if ex.statusCode == 404 => 43 | connectionLogger.info(formatMessage(ld, method, uri, startAge, s"failed ${ex.message}")) 44 | case Failure(ex) => connectionLogger.warn(formatMessage(ld, method, uri, startAge, s"failed ${ex.getMessage}")) 45 | } 46 | 47 | import uk.gov.hmrc.http.logging.ConnectionTracing.formatNs 48 | 49 | def formatMessage(ld: LoggingDetails, method: String, uri: String, startAge: Long, message: String): String = { 50 | val requestId = ld.requestId.getOrElse("") 51 | val requestChain = ld.requestChain 52 | val durationNs = ld.age - startAge 53 | s"$requestId:$method:$startAge:${formatNs(startAge)}:$durationNs:${formatNs(durationNs)}:${requestChain.value}:$uri:$message" 54 | } 55 | } 56 | 57 | object ConnectionTracing { 58 | def formatNs(ns: Long): String = { 59 | val nsPart = ns % 1000 60 | val usPart = ns / 1000 % 1000 61 | val msPart = ns / 1000000 % 1000 62 | val sPart = ns / 1000000000 63 | 64 | if (sPart > 0) f"${(sPart * 1000 + msPart) / 1000.0}%03.3fs" 65 | else if (msPart > 0) f"${(msPart * 1000 + usPart) / 1000.0}%03.3fms" 66 | else if (usPart > 0) f"${(usPart * 1000 + nsPart) / 1000.0}%03.3fus" 67 | else s"${ns}ns" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/logging/LoggingDetails.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http.logging 18 | 19 | import uk.gov.hmrc.http.{Authorization, ForwardedFor, HeaderNames, RequestChain, RequestId, SessionId} 20 | 21 | trait LoggingDetails { 22 | 23 | def sessionId: Option[SessionId] 24 | 25 | def requestId: Option[RequestId] 26 | 27 | def requestChain: RequestChain 28 | 29 | @deprecated("Authorization header is no longer included in logging", "-") 30 | def authorization: Option[Authorization] 31 | 32 | def forwarded: Option[ForwardedFor] 33 | 34 | def age: Long 35 | 36 | lazy val data: Map[String, Option[String]] = Map( 37 | HeaderNames.xRequestId -> requestId.map(_.value), 38 | HeaderNames.xSessionId -> sessionId.map(_.value), 39 | HeaderNames.xForwardedFor -> forwarded.map(_.value) 40 | ) 41 | 42 | def mdcData: Map[String, String] = 43 | for { 44 | d <- data 45 | v <- d._2 46 | } yield (d._1, v) 47 | } 48 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/http/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc 18 | 19 | import java.net.URL 20 | 21 | import sttp.model.UriInterpolator 22 | 23 | package object http { 24 | implicit class StringContextOps(val sc: StringContext) extends AnyVal { 25 | def url(args: Any*): URL = 26 | UriInterpolator.interpolate(sc, args: _*).toJavaUri.toURL 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/play/http/BodyCaptor.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.play.http 18 | 19 | import org.apache.pekko.NotUsed 20 | import org.apache.pekko.stream.{Attributes, FlowShape, Inlet, Outlet} 21 | import org.apache.pekko.stream.scaladsl.{Flow, Sink} 22 | import org.apache.pekko.stream.stage.{GraphStage, GraphStageLogic, InHandler, OutHandler} 23 | import org.apache.pekko.util.ByteString 24 | import uk.gov.hmrc.http.hooks.Data 25 | 26 | // based on play.filters.csrf.CSRFAction#BodyHandler 27 | 28 | private class BodyCaptorFlow( 29 | maxBodyLength : Int, 30 | withCapturedBody: Data[ByteString] => Unit 31 | ) extends GraphStage[FlowShape[ByteString, ByteString]] { 32 | val in = Inlet[ByteString]("BodyCaptorFlow.in") 33 | val out = Outlet[ByteString]("BodyCaptorFlow.out") 34 | override val shape = FlowShape.of(in, out) 35 | 36 | override def createLogic(inheritedAttributes: Attributes): GraphStageLogic = 37 | new GraphStageLogic(shape) { 38 | private var buffer = ByteString.empty 39 | 40 | setHandlers( 41 | in, 42 | out, 43 | new InHandler with OutHandler { 44 | override def onPull(): Unit = 45 | pull(in) 46 | 47 | override def onPush(): Unit = { 48 | val chunk = grab(in) 49 | if (buffer.size < maxBodyLength) 50 | buffer ++= chunk 51 | push(out, chunk) 52 | } 53 | 54 | override def onUpstreamFinish(): Unit = { 55 | withCapturedBody(BodyCaptor.bodyUpto(buffer, maxBodyLength)) 56 | completeStage() 57 | } 58 | } 59 | ) 60 | } 61 | } 62 | 63 | object BodyCaptor { 64 | def flow( 65 | maxBodyLength : Int, 66 | withCapturedBody: Data[ByteString] => Unit // provide a callback since a Materialized value would be not be available until the flow has been run 67 | ): Flow[ByteString, ByteString, NotUsed] = 68 | Flow.fromGraph(new BodyCaptorFlow( 69 | maxBodyLength = maxBodyLength, 70 | withCapturedBody = withCapturedBody 71 | )) 72 | 73 | def sink( 74 | maxBodyLength : Int, 75 | withCapturedBody: Data[ByteString] => Unit 76 | ): Sink[ByteString, NotUsed] = 77 | flow(maxBodyLength, withCapturedBody) 78 | .to(Sink.ignore) 79 | 80 | def bodyUpto(body: ByteString, maxBodyLength: Int): Data[ByteString] = 81 | if (body.length > maxBodyLength) 82 | Data.truncated(body.take(maxBodyLength)) 83 | else 84 | Data.pure(body) 85 | } 86 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/play/http/HeaderCarrierConverter.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.play.http 18 | 19 | import play.api.http.{HeaderNames => PlayHeaderNames} 20 | import play.api.mvc.{Cookies, Headers, RequestHeader, Session} 21 | import uk.gov.hmrc.http._ 22 | 23 | import scala.util.Try 24 | 25 | trait HeaderCarrierConverter { 26 | 27 | def fromRequest(request: RequestHeader): HeaderCarrier = 28 | buildHeaderCarrier( 29 | headers = request.headers, 30 | session = None, 31 | request = Some(request) 32 | ) 33 | 34 | def fromRequestAndSession(request: RequestHeader, session: Session): HeaderCarrier = 35 | buildHeaderCarrier( 36 | headers = request.headers, 37 | session = Some(session), 38 | request = Some(request) 39 | ) 40 | 41 | @deprecated("Use fromRequest or fromRequestAndSession as appropriate", "13.0.0") 42 | def fromHeadersAndSession(headers: Headers, session: Option[Session] = None): HeaderCarrier = 43 | buildHeaderCarrier(headers, session, request = None) 44 | 45 | @deprecated("Use fromRequest or fromRequestAndSession as appropriate", "13.0.0") 46 | def fromHeadersAndSessionAndRequest( 47 | headers: Headers, 48 | session: Option[Session] = None, 49 | request: Option[RequestHeader] = None 50 | ): HeaderCarrier = 51 | buildHeaderCarrier(headers, session, request) 52 | 53 | private def buildRequestChain(currentChain: Option[String]): RequestChain = 54 | currentChain 55 | .fold(RequestChain.init)(chain => RequestChain(chain).extend) 56 | 57 | private def requestTimestamp(headers: Headers): Long = 58 | headers 59 | .get(HeaderNames.xRequestTimestamp) 60 | .flatMap(tsAsString => Try(tsAsString.toLong).toOption) 61 | .getOrElse(System.nanoTime()) 62 | 63 | val Path = "path" 64 | 65 | private def lookupCookies(headers: Headers, request: Option[RequestHeader]): Cookies = { 66 | // Cookie setting changed between Play 2.5 and Play 2.6, this now checks both ways 67 | // cookie can be set for backwards compatibility 68 | val cookiesInHeader = 69 | Cookies.fromCookieHeader(headers.get(PlayHeaderNames.COOKIE)).toList 70 | val cookiesInSession = 71 | request.map(_.cookies).map(_.toList).getOrElse(List.empty) 72 | Cookies(cookiesInSession ++ cookiesInHeader) 73 | } 74 | 75 | private def buildHeaderCarrier( 76 | headers: Headers, 77 | session: Option[Session], 78 | request: Option[RequestHeader] 79 | ): HeaderCarrier = { 80 | lazy val cookies = lookupCookies(headers, request) 81 | HeaderCarrier( 82 | authorization = // Note, if a session is provided, any Authorization header in the request will be ignored 83 | session.fold(headers.get(HeaderNames.authorisation))(_.get(SessionKeys.authToken)) 84 | .map(Authorization.apply), 85 | forwarded = forwardedFor(headers), 86 | sessionId = session.flatMap(_.get(SessionKeys.sessionId)) 87 | .orElse(headers.get(HeaderNames.xSessionId)) 88 | .map(SessionId.apply), 89 | requestId = headers.get(HeaderNames.xRequestId).map(RequestId.apply), 90 | requestChain = buildRequestChain(headers.get(HeaderNames.xRequestChain)), 91 | nsStamp = requestTimestamp(headers), 92 | extraHeaders = Seq.empty, 93 | trueClientIp = headers.get(HeaderNames.trueClientIp), 94 | trueClientPort = headers.get(HeaderNames.trueClientPort), 95 | gaToken = headers.get(HeaderNames.googleAnalyticTokenId), 96 | gaUserId = headers.get(HeaderNames.googleAnalyticUserId), 97 | deviceID = session.flatMap(_ => cookies.get(CookieNames.deviceID).map(_.value)) 98 | .orElse(headers.get(HeaderNames.deviceID)), 99 | akamaiReputation = headers.get(HeaderNames.akamaiReputation).map(AkamaiReputation.apply), 100 | otherHeaders = otherHeaders(headers, request) 101 | ) 102 | } 103 | 104 | private def otherHeaders(headers: Headers, request: Option[RequestHeader]): Seq[(String, String)] = { 105 | val explicitlyIncludedHeadersLc = HeaderNames.explicitlyIncludedHeaders.map(_.toLowerCase) 106 | headers.headers 107 | .filterNot { case (k, _) => explicitlyIncludedHeadersLc.contains(k.toLowerCase) } ++ 108 | // adding path so that play-auditing can access the request path without a dependency on play 109 | request.map(rh => Path -> rh.path).toSeq 110 | } 111 | 112 | private def forwardedFor(headers: Headers): Option[ForwardedFor] = 113 | ((headers.get(HeaderNames.trueClientIp), headers.get(HeaderNames.xForwardedFor)) match { 114 | case (tcip, None) => tcip 115 | case (None | Some(""), xff) => xff 116 | case (Some(tcip), Some(xff)) if xff.startsWith(tcip) => Some(xff) 117 | case (Some(tcip), Some(xff)) => Some(s"$tcip, $xff") 118 | }).map(ForwardedFor.apply) 119 | } 120 | 121 | object HeaderCarrierConverter extends HeaderCarrierConverter 122 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/play/http/logging/Mdc.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.play.http.logging 18 | 19 | import org.slf4j.MDC 20 | 21 | import scala.concurrent.{ExecutionContext, Future} 22 | import scala.jdk.CollectionConverters._ 23 | 24 | object Mdc { 25 | 26 | def mdcData: Map[String, String] = 27 | Option(MDC.getCopyOfContextMap).map(_.asScala.toMap).getOrElse(Map.empty) 28 | 29 | def withMdc[A](block: => Future[A], mdcData: Map[String, String])(implicit ec: ExecutionContext): Future[A] = 30 | block.map { a => 31 | putMdc(mdcData) 32 | a 33 | }.recoverWith { 34 | case t => 35 | putMdc(mdcData) 36 | Future.failed(t) 37 | } 38 | 39 | def putMdc(mdc: Map[String, String]): Unit = 40 | mdc.foreach { 41 | case (k, v) => MDC.put(k, v) 42 | } 43 | 44 | /** Restores MDC data to the continuation of a block, which may be discarding MDC data (e.g. uses a different execution context) 45 | */ 46 | def preservingMdc[A](block: => Future[A])(implicit ec: ExecutionContext): Future[A] = 47 | withMdc(block, mdcData) 48 | } 49 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/play/http/logging/MdcLoggingExecutionContext.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.play.http.logging 18 | 19 | import org.slf4j.MDC 20 | 21 | import scala.concurrent.ExecutionContext 22 | 23 | @deprecated("MdcLoggingExecutionContext no longer required, please inject Play's default EC instead", "15.6.0") 24 | class MdcLoggingExecutionContext(wrapped: ExecutionContext, mdcData: Map[String, String]) 25 | extends ExecutionContext { 26 | 27 | def execute(runnable: Runnable): Unit = 28 | wrapped.execute(new RunWithMDC(runnable, mdcData)) 29 | 30 | private class RunWithMDC(runnable: Runnable, mdcData: Map[String, String]) extends Runnable { 31 | def run(): Unit = { 32 | val oldMdcData = MDC.getCopyOfContextMap 33 | MDC.clear() 34 | mdcData.foreach { 35 | case (k, v) => MDC.put(k, v) 36 | } 37 | try { 38 | runnable.run() 39 | } finally { 40 | if (oldMdcData == null) 41 | MDC.clear() 42 | else 43 | MDC.setContextMap(oldMdcData) 44 | } 45 | } 46 | } 47 | 48 | def reportFailure(t: Throwable): Unit = wrapped.reportFailure(t) 49 | } 50 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/play/http/ws/WSExecute.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.play.http.ws 18 | 19 | import scala.concurrent.ExecutionContext 20 | import uk.gov.hmrc.play.http.logging.Mdc 21 | 22 | @deprecated("Use HttpClientV2", "15.0.0") 23 | trait WSExecute { 24 | 25 | private[ws] def execute(req: play.api.libs.ws.WSRequest, method: String)(implicit ec: ExecutionContext) = 26 | // Since AHC internally uses a different execution context, providing a MDC enabled Execution context 27 | // will not preserve MDC data for further futures. 28 | // We will copy over the data manually to preserve them. 29 | Mdc.preservingMdc { 30 | req.withMethod(method).execute() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/play/http/ws/WSHttpResponse.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.play.http.ws 18 | 19 | import play.api.libs.ws.WSResponse 20 | import uk.gov.hmrc.http.HttpResponse 21 | 22 | @deprecated("Use HttpClientV2", "15.0.0") 23 | object WSHttpResponse { 24 | def apply(wsResponse: WSResponse): HttpResponse = 25 | HttpResponse( 26 | status = wsResponse.status, 27 | body = wsResponse.body, 28 | headers = forScala2_13(wsResponse.headers) 29 | ) 30 | 31 | // play returns scala.collection.Seq, but default for Scala 2.13 is scala.collection.immutable.Seq 32 | // duplicated from CollectionUtils since WSHttpResponse is not defined within uk.gov.hmrc.http package.. 33 | private def forScala2_13(m: Map[String, scala.collection.Seq[String]]): Map[String, Seq[String]] = 34 | // `m.mapValues(_.toSeq).toMap` by itself strips the ordering away 35 | scala.collection.immutable.TreeMap[String, Seq[String]]()(scala.math.Ordering.comparatorToOrdering(String.CASE_INSENSITIVE_ORDER)) ++ m.view.mapValues(_.toSeq) 36 | } 37 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/play/http/ws/WSRequest.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.play.http.ws 18 | 19 | import play.api.Configuration 20 | import play.api.libs.ws.{DefaultWSProxyServer, WSProxyServer, WSRequest => PlayWSRequest} 21 | 22 | @deprecated("Use HttpClientV2", "15.0.0") 23 | trait WSRequest extends WSRequestBuilder { 24 | 25 | override def buildRequest( 26 | url : String, 27 | headers: Seq[(String, String)] 28 | ): PlayWSRequest = 29 | wsClient.url(url) 30 | .withHttpHeaders(headers: _*) 31 | } 32 | 33 | @deprecated("Use HttpClientV2", "15.0.0") 34 | trait WSProxy extends WSRequest { 35 | 36 | def wsProxyServer: Option[WSProxyServer] 37 | 38 | override def buildRequest(url: String, headers: Seq[(String, String)]): PlayWSRequest = 39 | wsProxyServer match { 40 | case Some(proxy) => super.buildRequest(url, headers).withProxyServer(proxy) 41 | case None => super.buildRequest(url, headers) 42 | } 43 | } 44 | 45 | object WSProxyConfiguration { 46 | 47 | @deprecated("Use buildWsProxyServer instead. See docs for differences.", "14.0.0") 48 | def apply(configPrefix: String, configuration: Configuration): Option[WSProxyServer] = { 49 | val proxyRequired = 50 | configuration.getOptional[Boolean](s"$configPrefix.proxyRequiredForThisEnvironment").getOrElse(true) 51 | 52 | if (proxyRequired) 53 | Some( 54 | DefaultWSProxyServer( 55 | protocol = Some(configuration.get[String](s"$configPrefix.protocol")), 56 | host = configuration.get[String](s"$configPrefix.host"), 57 | port = configuration.get[Int](s"$configPrefix.port"), 58 | principal = configuration.getOptional[String](s"$configPrefix.username"), 59 | password = configuration.getOptional[String](s"$configPrefix.password") 60 | ) 61 | ) 62 | else None 63 | } 64 | 65 | def buildWsProxyServer(configuration: Configuration): Option[WSProxyServer] = 66 | if (configuration.get[Boolean]("http-verbs.proxy.enabled")) 67 | Some( 68 | DefaultWSProxyServer( 69 | protocol = Some(configuration.get[String]("proxy.protocol")), 70 | host = configuration.get[String]("proxy.host"), 71 | port = configuration.get[Int]("proxy.port"), 72 | principal = configuration.getOptional[String]("proxy.username"), 73 | password = configuration.getOptional[String]("proxy.password") 74 | ) 75 | ) 76 | else None 77 | } 78 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/play/http/ws/WSRequestBuilder.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.play.http.ws 18 | 19 | import play.api.libs.ws.{WSClient, WSRequest => PlayWSRequest} 20 | import uk.gov.hmrc.http.Request 21 | 22 | @deprecated("Use HttpClientV2", "15.0.0") 23 | trait WSRequestBuilder extends Request { 24 | 25 | protected def wsClient: WSClient 26 | 27 | protected def buildRequest(url: String, headers: Seq[(String, String)]): PlayWSRequest 28 | } 29 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/play/http/ws/default/WSDelete.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.play.http.ws.default 18 | 19 | import uk.gov.hmrc.http.{CoreDelete, DeleteHttpTransport, HttpResponse} 20 | import uk.gov.hmrc.play.http.ws.{WSExecute, WSHttpResponse, WSRequestBuilder} 21 | 22 | import scala.concurrent.{ExecutionContext, Future} 23 | 24 | @deprecated("Use HttpClientV2", "15.0.0") 25 | trait WSDelete extends CoreDelete with DeleteHttpTransport with WSRequestBuilder with WSExecute { 26 | 27 | override def doDelete( 28 | url : String, 29 | headers: Seq[(String, String)] 30 | )( 31 | implicit ec: ExecutionContext 32 | ): Future[HttpResponse] = 33 | execute(buildRequest(url, headers), "DELETE") 34 | .map(WSHttpResponse.apply) 35 | } 36 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/play/http/ws/default/WSGet.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.play.http.ws.default 18 | 19 | import uk.gov.hmrc.http.{CoreGet, GetHttpTransport, HttpResponse} 20 | import uk.gov.hmrc.play.http.ws.{WSExecute, WSHttpResponse, WSRequestBuilder} 21 | 22 | import scala.concurrent.{ExecutionContext, Future} 23 | 24 | @deprecated("Use HttpClientV2", "15.0.0") 25 | trait WSGet extends CoreGet with GetHttpTransport with WSRequestBuilder with WSExecute { 26 | 27 | override def doGet( 28 | url: String, 29 | headers: Seq[(String, String)] 30 | )( 31 | implicit ec: ExecutionContext 32 | ): Future[HttpResponse] = 33 | execute(buildRequest(url, headers), "GET") 34 | .map(WSHttpResponse.apply) 35 | } 36 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/play/http/ws/default/WSPatch.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.play.http.ws.default 18 | 19 | import play.api.libs.json.{Json, Writes} 20 | import uk.gov.hmrc.http.{CorePatch, HttpResponse, PatchHttpTransport} 21 | import uk.gov.hmrc.play.http.ws.{WSExecute, WSHttpResponse, WSRequestBuilder} 22 | import play.api.libs.ws.writeableOf_JsValue 23 | 24 | import scala.concurrent.{ExecutionContext, Future} 25 | 26 | @deprecated("Use HttpClientV2", "15.0.0") 27 | trait WSPatch extends CorePatch with PatchHttpTransport with WSRequestBuilder with WSExecute { 28 | 29 | override def doPatch[A]( 30 | url: String, 31 | body: A, 32 | headers: Seq[(String, String)] 33 | )( 34 | implicit rds: Writes[A], 35 | ec: ExecutionContext 36 | ): Future[HttpResponse] = 37 | execute(buildRequest(url, headers).withBody(Json.toJson(body)), "PATCH") 38 | .map(WSHttpResponse.apply) 39 | } 40 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/play/http/ws/default/WSPost.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.play.http.ws.default 18 | 19 | import play.api.libs.json.{Json, Writes} 20 | import play.api.libs.ws.WSRequest 21 | import uk.gov.hmrc.http.{CorePost, HttpResponse, PostHttpTransport} 22 | import uk.gov.hmrc.play.http.ws.{WSExecute, WSHttpResponse, WSRequestBuilder} 23 | import play.api.libs.ws.{writeableOf_JsValue, writeableOf_String, writeableOf_urlEncodedForm} 24 | 25 | import scala.concurrent.{ExecutionContext, Future} 26 | 27 | @deprecated("Use HttpClientV2", "15.0.0") 28 | trait WSPost extends CorePost with PostHttpTransport with WSRequestBuilder with WSExecute { 29 | 30 | def withEmptyBody(request: WSRequest): WSRequest 31 | 32 | override def doPost[A]( 33 | url: String, 34 | body: A, 35 | headers: Seq[(String, String)] 36 | )( 37 | implicit rds: Writes[A], 38 | ec: ExecutionContext 39 | ): Future[HttpResponse] = 40 | execute(buildRequest(url, headers).withBody(Json.toJson(body)), "POST") 41 | .map(WSHttpResponse.apply) 42 | 43 | override def doFormPost( 44 | url: String, 45 | body: Map[String, Seq[String]], 46 | headers: Seq[(String, String)] 47 | )( 48 | implicit ec: ExecutionContext 49 | ): Future[HttpResponse] = 50 | execute(buildRequest(url, headers).withBody(body), "POST") 51 | .map(WSHttpResponse.apply) 52 | 53 | override def doPostString( 54 | url: String, 55 | body: String, 56 | headers: Seq[(String, String)] 57 | )( 58 | implicit ec: ExecutionContext 59 | ): Future[HttpResponse] = 60 | execute(buildRequest(url, headers).withBody(body), "POST") 61 | .map(WSHttpResponse.apply) 62 | 63 | override def doEmptyPost[A]( 64 | url: String, 65 | headers: Seq[(String, String)] 66 | )( 67 | implicit ec: ExecutionContext 68 | ): Future[HttpResponse] = 69 | execute(withEmptyBody(buildRequest(url, headers)), "POST") 70 | .map(WSHttpResponse.apply) 71 | } 72 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/play/http/ws/default/WSPut.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.play.http.ws.default 18 | 19 | import play.api.libs.json.{Json, Writes} 20 | import uk.gov.hmrc.http.{CorePut, HttpResponse, PutHttpTransport} 21 | import uk.gov.hmrc.play.http.ws.{WSExecute, WSHttpResponse, WSRequestBuilder} 22 | import play.api.libs.ws.{writeableOf_JsValue, writeableOf_String} 23 | 24 | import scala.concurrent.{ExecutionContext, Future} 25 | 26 | @deprecated("Use HttpClientV2", "15.0.0") 27 | trait WSPut extends CorePut with PutHttpTransport with WSRequestBuilder with WSExecute{ 28 | 29 | override def doPut[A]( 30 | url: String, 31 | body: A, 32 | headers: Seq[(String, String)] 33 | )( 34 | implicit rds: Writes[A], 35 | ec: ExecutionContext 36 | ): Future[HttpResponse] = 37 | execute(buildRequest(url, headers).withBody(Json.toJson(body)), "PUT") 38 | .map(WSHttpResponse.apply) 39 | 40 | override def doPutString( 41 | url: String, 42 | body: String, 43 | headers: Seq[(String, String)] 44 | )( 45 | implicit ec: ExecutionContext 46 | ): Future[HttpResponse] = 47 | execute(buildRequest(url, headers).withBody(body), "PUT") 48 | .map(WSHttpResponse.apply) 49 | } 50 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/main/scala/uk/gov/hmrc/play/http/ws/verbs.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.play.http.ws 18 | 19 | import play.api.libs.ws.{EmptyBody, WSRequest => PlayWSRequest} 20 | import play.api.libs.ws.writeableOf_WsBody 21 | 22 | @deprecated("Use HttpClientV2", "15.0.0") 23 | trait WSDelete extends default.WSDelete with WSRequest 24 | 25 | @deprecated("Use HttpClientV2", "15.0.0") 26 | trait WSGet extends default.WSGet with WSRequest 27 | 28 | @deprecated("Use HttpClientV2", "15.0.0") 29 | trait WSPatch extends default.WSPatch with WSRequest 30 | 31 | @deprecated("Use HttpClientV2", "15.0.0") 32 | trait WSPut extends default.WSPut with WSRequest 33 | 34 | @deprecated("Use HttpClientV2", "15.0.0") 35 | trait WSPost extends default.WSPost with WSRequest { 36 | override def withEmptyBody(request: PlayWSRequest): PlayWSRequest = 37 | request.withBody(EmptyBody).addHttpHeaders((play.api.http.HeaderNames.CONTENT_LENGTH -> "0")) 38 | } 39 | 40 | @deprecated("Use HttpClientV2", "15.0.0") 41 | trait WSHttp extends WSGet with WSPut with WSPost with WSDelete with WSPatch 42 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %date %highlight([%level]) [%logger] [%thread] %message%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/test/scala/uk/gov/hmrc/http/CommonHttpBehaviour.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | import org.scalatest.concurrent.ScalaFutures 20 | import org.scalatest.wordspec.AnyWordSpecLike 21 | import org.scalatest.matchers.should.Matchers 22 | import play.api.libs.json.{Json, OFormat} 23 | import uk.gov.hmrc.http.logging.{ConnectionTracing, LoggingDetails} 24 | 25 | import java.net.ConnectException 26 | import java.util.concurrent.TimeoutException 27 | import scala.collection.mutable 28 | import scala.concurrent.{ExecutionContext, Future} 29 | 30 | trait CommonHttpBehaviour extends ScalaFutures with Matchers with AnyWordSpecLike { 31 | 32 | case class TestClass(foo: String, bar: Int) 33 | implicit val tcreads: OFormat[TestClass] = Json.format[TestClass] 34 | 35 | case class TestRequestClass(baz: String, bar: Int) 36 | implicit val trcreads: OFormat[TestRequestClass] = Json.format[TestRequestClass] 37 | 38 | implicit val hc: HeaderCarrier = HeaderCarrier() 39 | val testBody: String = "testBody" 40 | val testRequestBody: String = "testRequestBody" 41 | val url: String = "http://some.url" 42 | 43 | def response(returnValue: Option[String] = None, statusCode: Int = 200): Future[HttpResponse] = 44 | Future.successful(HttpResponse( 45 | status = statusCode, 46 | body = returnValue.getOrElse("") 47 | )) 48 | 49 | val defaultHttpResponse: Future[HttpResponse] = response() 50 | 51 | def anErrorMappingHttpCall(verb: String, httpCall: (String, Future[HttpResponse]) => Future[_]): Unit = { 52 | s"throw a GatewayTimeout exception when the HTTP $verb throws a TimeoutException" in { 53 | 54 | val url: String = "http://some.nonexistent.url" 55 | 56 | val e = httpCall(url, Future.failed(new TimeoutException("timeout"))).failed.futureValue 57 | 58 | e should be(a[GatewayTimeoutException]) 59 | e.getMessage should startWith(verb) 60 | e.getMessage should include(url) 61 | } 62 | 63 | s"throw a BadGateway exception when the HTTP $verb throws a ConnectException" in { 64 | 65 | val url: String = "http://some.nonexistent.url" 66 | 67 | val e = httpCall(url, Future.failed(new ConnectException("timeout"))).failed.futureValue 68 | 69 | e should be(a[BadGatewayException]) 70 | e.getMessage should startWith(verb) 71 | e.getMessage should include(url) 72 | } 73 | } 74 | 75 | def aTracingHttpCall[T <: ConnectionTracingCapturing](verb: String, method: String, httpBuilder: => T)( 76 | httpAction: T => Future[_]): Unit = 77 | s"trace exactly once when the HTTP $verb calls $method" in { 78 | val http = httpBuilder 79 | httpAction(http).futureValue 80 | http.traceCalls should have size 1 81 | http.traceCalls.head._1 shouldBe verb 82 | } 83 | 84 | } 85 | 86 | trait ConnectionTracingCapturing extends ConnectionTracing { 87 | 88 | val traceCalls: mutable.Buffer[(String, String)] = mutable.Buffer[(String, String)]() 89 | 90 | override def withTracing[T](method: String, uri: String)( 91 | body: => Future[T])(implicit ld: LoggingDetails, ec: ExecutionContext): Future[T] = { 92 | traceCalls += ((method, uri)) 93 | body 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/test/scala/uk/gov/hmrc/http/HeaderCarrierSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | import com.typesafe.config.ConfigFactory 20 | import org.scalactic.StringNormalizations.lowerCased 21 | import org.scalatest.LoneElement 22 | import org.scalatest.matchers.should.Matchers 23 | import org.scalatest.wordspec.AnyWordSpecLike 24 | import org.scalatestplus.mockito.MockitoSugar 25 | 26 | class HeaderCarrierSpec 27 | extends AnyWordSpecLike 28 | with Matchers 29 | with MockitoSugar 30 | with LoneElement { 31 | 32 | "headersForUrl" should { 33 | val internalUrls = List( 34 | "http://test.public.service/bar", 35 | "http://test.public.mdtp/bar", 36 | "http://localhost:1234/bar" 37 | ) 38 | val externalUrl = "http://test.me" 39 | 40 | def mkConfig(s: String = ""): HeaderCarrier.Config = 41 | HeaderCarrier.Config.fromConfig( 42 | ConfigFactory.parseString(s) 43 | .withFallback(ConfigFactory.load()) 44 | ) 45 | 46 | "should contain the values passed in by header-carrier for internal urls" in { 47 | val hc = HeaderCarrier( 48 | authorization = Some(Authorization("auth")), 49 | sessionId = Some(SessionId("session")), 50 | requestId = Some(RequestId("request")), 51 | forwarded = Some(ForwardedFor("forwarded")) 52 | ) 53 | 54 | internalUrls.map { url => 55 | val result = hc.headersForUrl(mkConfig())(url) 56 | 57 | Seq( 58 | HeaderNames.authorisation -> "auth", 59 | HeaderNames.xSessionId -> "session", 60 | HeaderNames.xRequestId -> "request", 61 | HeaderNames.xForwardedFor -> "forwarded" 62 | ).map(hdr => result should contain (hdr)) 63 | } 64 | } 65 | 66 | "should not contain the values passed in by header-carrier for external urls, if no config" in { 67 | val hc = HeaderCarrier( 68 | authorization = Some(Authorization("auth")), 69 | sessionId = Some(SessionId("session")), 70 | requestId = Some(RequestId("request")), 71 | forwarded = Some(ForwardedFor("forwarded")) 72 | ) 73 | 74 | val result = hc.headersForUrl(mkConfig())(url = externalUrl) 75 | 76 | Seq( 77 | HeaderNames.authorisation, 78 | HeaderNames.xSessionId, 79 | HeaderNames.xRequestId, 80 | HeaderNames.xForwardedFor 81 | ).map(k => (result.map(_._1) should not contain k) (after being lowerCased)) 82 | } 83 | 84 | "should include the User-Agent header when the 'appName' config value is present" in { 85 | val config = mkConfig("appName: myApp") 86 | 87 | (externalUrl :: internalUrls).map { url => 88 | val result = HeaderCarrier().headersForUrl(config)(url) 89 | 90 | result should contain ("User-Agent" -> "myApp") 91 | } 92 | } 93 | 94 | "filter 'other headers' from request for external service calls" in { 95 | val hc = HeaderCarrier( 96 | otherHeaders = Seq("foo" -> "secret!") 97 | ) 98 | 99 | val result = hc.headersForUrl(mkConfig())(url = externalUrl) 100 | 101 | (result.map(_._1) should not contain "foo") (after being lowerCased) 102 | } 103 | 104 | "filter 'other headers' in request for internal urls, if no allowlist provided" in { 105 | val hc = HeaderCarrier( 106 | otherHeaders = Seq("foo" -> "secret!") 107 | ) 108 | 109 | internalUrls.map { url => 110 | val result = hc.headersForUrl(mkConfig())(url) 111 | (result.map(_._1) should not contain "foo") (after being lowerCased) 112 | } 113 | } 114 | 115 | "filter 'other headers' in request for internal urls, if not in provided allowlist" in { 116 | val hc = HeaderCarrier( 117 | otherHeaders = Seq("foo" -> "secret!") 118 | ) 119 | 120 | val config = mkConfig("bootstrap.http.headersAllowlist: []") 121 | 122 | internalUrls.map { url => 123 | val result = hc.headersForUrl(config)(url) 124 | (result.map(_._1) should not contain "foo") (after being lowerCased) 125 | } 126 | } 127 | 128 | "include 'remaining headers' in request for internal urls, if in provided allowlist" in { 129 | val hc = HeaderCarrier( 130 | otherHeaders = Seq("foo" -> "secret!") 131 | ) 132 | 133 | val config = 134 | mkConfig("bootstrap.http.headersAllowlist: [foo]") 135 | 136 | internalUrls.map { url => 137 | val result = hc.headersForUrl(config)(url) 138 | result should contain ("foo" -> "secret!") 139 | } 140 | } 141 | 142 | "include 'remaining headers' in request for internal service call to other configured internal URL pattern" in { 143 | val url = "http://localhost/foo" // an internal service call, according to config 144 | val hc = HeaderCarrier( 145 | otherHeaders = Seq("foo" -> "secret!") 146 | ) 147 | 148 | val config = mkConfig("bootstrap.http.headersAllowlist: [foo]") 149 | 150 | val result = hc.headersForUrl(config)(url) 151 | result should contain ("foo" -> "secret!") 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/test/scala/uk/gov/hmrc/http/HttpDeleteSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | import com.typesafe.config.{Config, ConfigFactory} 20 | import org.apache.pekko.actor.ActorSystem 21 | import org.mockito.ArgumentCaptor 22 | import org.mockito.ArgumentMatchers.{any, eq => eqTo} 23 | import org.mockito.Mockito.verify 24 | import org.scalatest.concurrent.PatienceConfiguration.{Interval, Timeout} 25 | import org.scalatest.time.{Millis, Seconds, Span} 26 | import org.scalatest.wordspec.AnyWordSpecLike 27 | import org.scalatest.matchers.should.Matchers 28 | import uk.gov.hmrc.http.hooks.{Data, HttpHook, RequestData, ResponseData} 29 | import org.scalatestplus.mockito.MockitoSugar 30 | 31 | import scala.concurrent.{ExecutionContext, Future} 32 | 33 | import uk.gov.hmrc.http.HttpReads.Implicits._ 34 | 35 | @annotation.nowarn("msg=deprecated") 36 | class HttpDeleteSpec 37 | extends AnyWordSpecLike 38 | with Matchers 39 | with MockitoSugar 40 | with CommonHttpBehaviour { 41 | 42 | import ExecutionContext.Implicits.global 43 | 44 | class StubbedHttpDelete( 45 | doDeleteResult: Future[HttpResponse] 46 | ) extends HttpDelete 47 | with DeleteHttpTransport 48 | with ConnectionTracingCapturing { 49 | 50 | val testHook1: HttpHook = mock[HttpHook] 51 | val testHook2: HttpHook = mock[HttpHook] 52 | val hooks = Seq(testHook1, testHook2) 53 | override val configuration: Config = ConfigFactory.load() 54 | override protected val actorSystem: ActorSystem = ActorSystem("test-actor-system") 55 | 56 | def appName: String = ??? 57 | 58 | override def doDelete( 59 | url: String, 60 | headers: Seq[(String, String)])( 61 | implicit ec: ExecutionContext): Future[HttpResponse] = 62 | doDeleteResult 63 | } 64 | 65 | class UrlTestingHttpDelete() extends HttpDelete with DeleteHttpTransport { 66 | 67 | var lastUrl: Option[String] = None 68 | 69 | override val configuration: Config = ConfigFactory.load() 70 | 71 | override protected val actorSystem: ActorSystem = ActorSystem("test-actor-system") 72 | 73 | override def doDelete( 74 | url: String, 75 | headers: Seq[(String, String)])( 76 | implicit ec: ExecutionContext): Future[HttpResponse] = { 77 | lastUrl = Some(url) 78 | defaultHttpResponse 79 | } 80 | 81 | override val hooks: Seq[HttpHook] = Seq() 82 | } 83 | 84 | "HttpDelete" should { 85 | "return plain responses" in { 86 | val response = HttpResponse(200, testBody) 87 | val testDelete = new StubbedHttpDelete(Future.successful(response)) 88 | testDelete.DELETE[HttpResponse](url, Seq("foo" -> "bar")).futureValue shouldBe response 89 | } 90 | 91 | "return objects deserialised from JSON" in { 92 | val testDelete = new StubbedHttpDelete(Future.successful(HttpResponse(200, """{"foo":"t","bar":10}"""))) 93 | testDelete 94 | .DELETE[TestClass](url, Seq("foo" -> "bar")) 95 | .futureValue(Timeout(Span(2, Seconds)), Interval(Span(15, Millis))) shouldBe TestClass("t", 10) 96 | } 97 | 98 | "return a url with encoded param pairs with url builder" in { 99 | val expected = 100 | Some("http://test.net?email=test%2Balias@email.com&data=%7B%22message%22:%22in+json+format%22%7D") 101 | val testDelete = new UrlTestingHttpDelete() 102 | val queryParams = Seq("email" -> "test+alias@email.com", "data" -> "{\"message\":\"in json format\"}") 103 | testDelete.DELETE[HttpResponse](url"http://test.net?$queryParams") 104 | testDelete.lastUrl shouldBe expected 105 | } 106 | 107 | "return an encoded url when query param is in baseUrl" in { 108 | val expected = 109 | Some("http://test.net?email=testalias@email.com&foo=bar&data=%7B%22message%22:%22in+json+format%22%7D") 110 | val testDelete = new UrlTestingHttpDelete() 111 | val queryParams = Seq("data" -> "{\"message\":\"in json format\"}") 112 | testDelete 113 | .DELETE[HttpResponse](url"http://test.net?email=testalias@email.com&foo=bar&$queryParams") 114 | testDelete.lastUrl shouldBe expected 115 | } 116 | 117 | "return encoded url when query params are already encoded" in { 118 | val expected = 119 | Some("http://test.net?email=test%2Balias@email.com") 120 | val testDelete = new UrlTestingHttpDelete() 121 | testDelete 122 | .DELETE[HttpResponse](url"http://test.net?email=test%2Balias@email.com") 123 | testDelete.lastUrl shouldBe expected 124 | } 125 | 126 | "return encoded url when path needs encoding" in { 127 | val expected = 128 | Some("http://test.net/some%2Fother%2Froute%3Fa=b&c=d%23/something?email=testalias@email.com") 129 | val testDelete = new UrlTestingHttpDelete() 130 | val paths = List("some/other/route?a=b&c=d#", "something") 131 | val email = "testalias@email.com" 132 | testDelete.DELETE[HttpResponse](url"http://test.net/$paths?email=$email") 133 | testDelete.lastUrl shouldBe expected 134 | } 135 | 136 | behave like anErrorMappingHttpCall("DELETE", (url, responseF) => new StubbedHttpDelete(responseF).DELETE[HttpResponse](url, Seq("foo" -> "bar"))) 137 | behave like aTracingHttpCall("DELETE", "DELETE", new StubbedHttpDelete(defaultHttpResponse)) { _.DELETE[HttpResponse](url, Seq("foo" -> "bar")) } 138 | 139 | "Invoke any hooks provided" in { 140 | val dummyResponse = HttpResponse(200, testBody) 141 | val dummyResponseFuture = Future.successful(dummyResponse) 142 | val testDelete = new StubbedHttpDelete(dummyResponseFuture) 143 | 144 | testDelete.DELETE[HttpResponse](url, Seq("header" -> "foo")).futureValue 145 | 146 | val responseFCaptor1 = ArgumentCaptor.forClass(classOf[Future[ResponseData]]) 147 | val responseFCaptor2 = ArgumentCaptor.forClass(classOf[Future[ResponseData]]) 148 | 149 | val requestCaptor1 = ArgumentCaptor.forClass(classOf[RequestData]) 150 | val requestCaptor2 = ArgumentCaptor.forClass(classOf[RequestData]) 151 | 152 | val config = HeaderCarrier.Config.fromConfig(testDelete.configuration) 153 | val headers = HeaderCarrier.headersForUrl(config, url, Seq("header" -> "foo")) 154 | 155 | verify(testDelete.testHook1).apply(eqTo("DELETE"), eqTo(url"$url"), requestCaptor1.capture(), responseFCaptor1.capture())(any[HeaderCarrier], any[ExecutionContext]) 156 | verify(testDelete.testHook2).apply(eqTo("DELETE"), eqTo(url"$url"), requestCaptor2.capture(), responseFCaptor2.capture())(any[HeaderCarrier], any[ExecutionContext]) 157 | 158 | val request1 = requestCaptor1.getValue 159 | request1.headers should contain allElementsOf(headers) 160 | request1.body shouldBe None 161 | 162 | val request2 = requestCaptor2.getValue 163 | request2.headers should contain allElementsOf(headers) 164 | request2.body shouldBe None 165 | 166 | // verifying directly without ArgCaptor doesn't work since Futures are different instances 167 | // e.g. Future.successful(5) != Future.successful(5) 168 | val response1 = responseFCaptor1.getValue.futureValue 169 | response1.status shouldBe 200 170 | response1.body shouldBe Data.pure(testBody) 171 | 172 | val response2 = responseFCaptor2.getValue.futureValue 173 | response2.status shouldBe 200 174 | response2.body shouldBe Data.pure(testBody) 175 | } 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/test/scala/uk/gov/hmrc/http/HttpErrorFunctionsSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | import org.scalacheck.{Gen, Shrink} 20 | import org.scalatest.EitherValues 21 | import org.scalatest.matchers.should.Matchers 22 | import org.scalatest.prop.TableDrivenPropertyChecks 23 | import org.scalatest.wordspec.AnyWordSpec 24 | import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks 25 | 26 | class HttpErrorFunctionsSpec 27 | extends AnyWordSpec 28 | with Matchers 29 | with ScalaCheckDrivenPropertyChecks 30 | with TableDrivenPropertyChecks 31 | with EitherValues { 32 | 33 | // Disable shrinking 34 | implicit def noShrink[T]: Shrink[T] = Shrink.shrinkAny 35 | 36 | "HttpErrorFunctions.handleResponseEither" should { 37 | "return the response if the status code is between 200 and 299" in { 38 | forAll(Gen.choose(200, 299))(expectResponse) 39 | } 40 | 41 | "return the exception for 4xx, 5xx" in { 42 | forAll(Gen.choose(400, 499))(expectError(_, 500)) 43 | forAll(Gen.choose(500, 599))(expectError(_, 502)) 44 | } 45 | 46 | "return the response for other codes" in { 47 | forAll(Gen.choose(0, 399))(expectResponse) 48 | forAll(Gen.choose(600, 1000))(expectResponse) 49 | } 50 | } 51 | 52 | val exampleVerb = "GET" 53 | val exampleUrl = "http://example.com/something" 54 | val exampleBody = "this is the string body" 55 | 56 | def expectError(statusCode: Int, reportAs: Int): Unit = 57 | new HttpErrorFunctions { 58 | val e = handleResponseEither(exampleVerb, exampleUrl)(HttpResponse(statusCode, exampleBody)).left.value 59 | e.getMessage should (include(exampleUrl) and include(exampleVerb) and include(exampleBody)) 60 | e.statusCode shouldBe statusCode 61 | e.reportAs shouldBe reportAs 62 | } 63 | 64 | def expectResponse(forStatus: Int): Unit = 65 | new HttpErrorFunctions { 66 | val expectedResponse = HttpResponse(forStatus, "") 67 | handleResponseEither(exampleVerb, exampleUrl)(expectedResponse).value shouldBe expectedResponse 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/test/scala/uk/gov/hmrc/http/HttpPatchSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | import com.typesafe.config.{Config, ConfigFactory} 20 | import org.apache.pekko.actor.ActorSystem 21 | import org.mockito.ArgumentCaptor 22 | import org.mockito.ArgumentMatchers.{any, eq => eqTo} 23 | import org.mockito.Mockito.verify 24 | import org.scalatest.wordspec.AnyWordSpecLike 25 | import org.scalatest.matchers.should.Matchers 26 | import play.api.libs.json.{Json, Writes} 27 | import uk.gov.hmrc.http.hooks.{Data, HookData, HttpHook, RequestData, ResponseData} 28 | import org.scalatestplus.mockito.MockitoSugar 29 | 30 | import scala.concurrent.{ExecutionContext, Future} 31 | 32 | import uk.gov.hmrc.http.HttpReads.Implicits._ 33 | 34 | @annotation.nowarn("msg=deprecated") 35 | class HttpPatchSpec 36 | extends AnyWordSpecLike 37 | with Matchers 38 | with MockitoSugar 39 | with CommonHttpBehaviour { 40 | import ExecutionContext.Implicits.global 41 | 42 | class StubbedHttpPatch( 43 | doPatchResult : Future[HttpResponse], 44 | doPatchWithHeaderResult: Future[HttpResponse] 45 | ) extends HttpPatch 46 | with ConnectionTracingCapturing { 47 | 48 | val testHook1: HttpHook = mock[HttpHook] 49 | val testHook2: HttpHook = mock[HttpHook] 50 | val hooks = Seq(testHook1, testHook2) 51 | override val configuration: Config = ConfigFactory.load() 52 | override protected val actorSystem: ActorSystem = ActorSystem("test-actor-system") 53 | 54 | override def doPatch[A]( 55 | url: String, 56 | body: A, 57 | headers: Seq[(String, String)])( 58 | implicit rds: Writes[A], 59 | ec: ExecutionContext): Future[HttpResponse] = 60 | doPatchResult 61 | } 62 | 63 | class UrlTestingHttpPatch() extends HttpPatch with PatchHttpTransport { 64 | var lastUrl: Option[String] = None 65 | 66 | override val configuration: Config = ConfigFactory.load() 67 | 68 | override protected val actorSystem: ActorSystem = ActorSystem("test-actor-system") 69 | 70 | override def doPatch[A]( 71 | url: String, 72 | body: A, 73 | headers: Seq[(String, String)])( 74 | implicit rds: Writes[A], 75 | ec: ExecutionContext): Future[HttpResponse] = { 76 | lastUrl = Some(url) 77 | defaultHttpResponse 78 | } 79 | 80 | override val hooks: Seq[HttpHook] = Seq() 81 | } 82 | 83 | "HttpPatch" should { 84 | val testObject = TestRequestClass("a", 1) 85 | 86 | "return plain responses" in { 87 | val response = HttpResponse(200, testBody) 88 | val testPatch = new StubbedHttpPatch(Future.successful(response), Future.successful(response)) 89 | testPatch.PATCH[TestRequestClass, HttpResponse](url, testObject, Seq("header" -> "foo")).futureValue shouldBe response 90 | } 91 | 92 | "return objects deserialised from JSON" in { 93 | val response= Future.successful(HttpResponse(200, """{"foo":"t","bar":10}""")) 94 | val testPatch = new StubbedHttpPatch(response, response) 95 | testPatch.PATCH[TestRequestClass, TestClass](url, testObject, Seq("header" -> "foo")).futureValue should be(TestClass("t", 10)) 96 | } 97 | 98 | "return a url with encoded param pairs with url builder" in { 99 | val expected = 100 | Some("http://test.net?email=test%2Balias@email.com&data=%7B%22message%22:%22in+json+format%22%7D") 101 | val testPatch = new UrlTestingHttpPatch() 102 | val queryParams = Seq("email" -> "test+alias@email.com", "data" -> "{\"message\":\"in json format\"}") 103 | testPatch.PATCH[TestRequestClass, HttpResponse](url"http://test.net?$queryParams", testObject) 104 | testPatch.lastUrl shouldBe expected 105 | } 106 | 107 | "return an encoded url when query param is in baseUrl" in { 108 | val expected = 109 | Some("http://test.net?email=testalias@email.com&foo=bar&data=%7B%22message%22:%22in+json+format%22%7D") 110 | val testPatch = new UrlTestingHttpPatch() 111 | val queryParams = Seq("data" -> "{\"message\":\"in json format\"}") 112 | testPatch 113 | .PATCH[TestRequestClass, HttpResponse](url"http://test.net?email=testalias@email.com&foo=bar&$queryParams", testObject) 114 | testPatch.lastUrl shouldBe expected 115 | } 116 | 117 | "return encoded url when query params are already encoded" in { 118 | val expected = 119 | Some("http://test.net?email=test%2Balias@email.com") 120 | val testPatch = new UrlTestingHttpPatch() 121 | testPatch 122 | .PATCH[TestRequestClass, HttpResponse](url"http://test.net?email=test%2Balias@email.com", testObject) 123 | testPatch.lastUrl shouldBe expected 124 | } 125 | 126 | "return encoded url when path needs encoding" in { 127 | val expected = 128 | Some("http://test.net/some%2Fother%2Froute%3Fa=b&c=d%23/something?email=testalias@email.com") 129 | val testPatch = new UrlTestingHttpPatch() 130 | val paths = List("some/other/route?a=b&c=d#", "something") 131 | val email = "testalias@email.com" 132 | testPatch.PATCH[TestRequestClass, HttpResponse](url"http://test.net/$paths?email=$email", testObject) 133 | testPatch.lastUrl shouldBe expected 134 | } 135 | 136 | behave like anErrorMappingHttpCall( 137 | "PATCH", 138 | (url, responseF) => new StubbedHttpPatch(responseF, responseF).PATCH[TestRequestClass, HttpResponse](url, testObject, Seq("header" -> "foo"))) 139 | behave like aTracingHttpCall("PATCH", "PATCH", new StubbedHttpPatch(defaultHttpResponse, defaultHttpResponse)) { 140 | _.PATCH[TestRequestClass, HttpResponse](url, testObject, Seq("header" -> "foo")) 141 | } 142 | 143 | "Invoke any hooks provided" in { 144 | val dummyResponse = HttpResponse(200, testBody) 145 | val dummyResponseFuture = Future.successful(dummyResponse) 146 | val testPatch = new StubbedHttpPatch(dummyResponseFuture, dummyResponseFuture) 147 | val testJson = Json.stringify(trcreads.writes(testObject)) 148 | 149 | testPatch.PATCH[TestRequestClass, HttpResponse](url, testObject, Seq("header" -> "foo")).futureValue 150 | 151 | val responseFCaptor1 = ArgumentCaptor.forClass(classOf[Future[ResponseData]]) 152 | val responseFCaptor2 = ArgumentCaptor.forClass(classOf[Future[ResponseData]]) 153 | 154 | val requestCaptor1 = ArgumentCaptor.forClass(classOf[RequestData]) 155 | val requestCaptor2 = ArgumentCaptor.forClass(classOf[RequestData]) 156 | 157 | val config = HeaderCarrier.Config.fromConfig(testPatch.configuration) 158 | val headers = HeaderCarrier.headersForUrl(config, url, Seq("header" -> "foo")) 159 | 160 | verify(testPatch.testHook1).apply(eqTo("PATCH"), eqTo(url"$url"), requestCaptor1.capture(), responseFCaptor1.capture())(any[HeaderCarrier], any[ExecutionContext]) 161 | verify(testPatch.testHook2).apply(eqTo("PATCH"), eqTo(url"$url"), requestCaptor2.capture(), responseFCaptor2.capture())(any[HeaderCarrier], any[ExecutionContext]) 162 | 163 | val request1 = requestCaptor1.getValue 164 | request1.headers should contain allElementsOf(headers) 165 | request1.body shouldBe Some(Data.pure(HookData.FromString(testJson))) 166 | 167 | val request2 = requestCaptor2.getValue 168 | request2.headers should contain allElementsOf(headers) 169 | request2.body shouldBe Some(Data.pure(HookData.FromString(testJson))) 170 | 171 | // verifying directly without ArgCaptor doesn't work since Futures are different instances 172 | // e.g. Future.successful(5) != Future.successful(5) 173 | val response1 = responseFCaptor1.getValue.futureValue 174 | response1.status shouldBe 200 175 | response1.body shouldBe Data.pure(testBody) 176 | 177 | val response2 = responseFCaptor2.getValue.futureValue 178 | response2.status shouldBe 200 179 | response2.body shouldBe Data.pure(testBody) 180 | } 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/test/scala/uk/gov/hmrc/http/HttpReadsLegacyInstancesSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | import org.scalacheck.Gen 20 | import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks 21 | import org.scalatest.wordspec.AnyWordSpec 22 | import org.scalatest.matchers.should.Matchers 23 | import play.api.libs.json.{Json, Reads} 24 | 25 | @annotation.nowarn("msg=deprecated") 26 | class HttpReadsLegacyInstancesSpec extends AnyWordSpec with ScalaCheckDrivenPropertyChecks with Matchers { 27 | "RawReads" should { 28 | val reads = HttpReads.readRaw 29 | "return the bare response if success" in { 30 | forAll(Gen.choose(200, 299)) { s => 31 | val response = exampleResponse(s) 32 | reads.read(exampleVerb, exampleUrl, response) should be(response) 33 | } 34 | } 35 | "throw exception for non-success codes" in { 36 | forAll(Gen.choose(400, 599)) { s => 37 | val errorResponse = exampleResponse(s) 38 | an[Exception] should be thrownBy reads.read(exampleVerb, exampleUrl, errorResponse) 39 | } 40 | } 41 | } 42 | 43 | "OptionHttpReads" should { 44 | "return None if the status code is 204 or 404" in { 45 | val otherReads = new HttpReads[String] { 46 | def read(method: String, url: String, response: HttpResponse) = fail("called the nested reads") 47 | } 48 | val reads = HttpReads.readOptionOf(otherReads) 49 | 50 | reads.read(exampleVerb, exampleUrl, exampleResponse(204)) should be(None) 51 | reads.read(exampleVerb, exampleUrl, exampleResponse(404)) should be(None) 52 | } 53 | 54 | "defer to the nested reads otherwise" in { 55 | val otherReads = new HttpReads[String] { 56 | def read(method: String, url: String, response: HttpResponse) = "hi" 57 | } 58 | val reads = HttpReads.readOptionOf(otherReads) 59 | 60 | forAll(Gen.posNum[Int].filter(_ != 204).filter(_ != 404)) { s => 61 | reads.read(exampleVerb, exampleUrl, exampleResponse(s)) should be(Some("hi")) 62 | } 63 | } 64 | } 65 | 66 | implicit val r: Reads[Example] = Json.reads[Example] 67 | "JsonHttpReads.readFromJson" should { 68 | val reads = HttpReads.readFromJson[Example] 69 | "convert a successful response body to the given class" in { 70 | val response = HttpResponse( 71 | status = 200, 72 | json = Json.obj("v1" -> "test", "v2" -> 5), 73 | headers = Map.empty 74 | ) 75 | reads.read(exampleVerb, exampleUrl, response) should be(Example("test", 5)) 76 | } 77 | 78 | "convert a successful response body with json that doesn't validate into an exception" in { 79 | val response = HttpResponse( 80 | status = 200, 81 | json = Json.obj("v1" -> "test"), 82 | headers = Map.empty 83 | ) 84 | a[JsValidationException] should be thrownBy reads.read(exampleVerb, exampleUrl, response) 85 | } 86 | 87 | "throw Exception for any failure" in { 88 | forAll(Gen.posNum[Int].filter(!_.toString.startsWith("2"))) { s => 89 | an[Exception] should be thrownBy reads.read(exampleVerb, exampleUrl, exampleResponse(s)) 90 | } 91 | } 92 | } 93 | 94 | "JsonHttpReads.readSeqFromJsonProperty" should { 95 | val reads = HttpReads.readSeqFromJsonProperty[Example]("items") 96 | "convert a successful response body to the given class" in { 97 | val response = HttpResponse( 98 | status = 200, 99 | json = Json.obj( 100 | "items" -> 101 | Json.arr( 102 | Json.obj("v1" -> "test", "v2" -> 1), 103 | Json.obj("v1" -> "test", "v2" -> 2) 104 | ) 105 | ), 106 | headers = Map.empty 107 | ) 108 | reads.read(exampleVerb, exampleUrl, response) should 109 | contain theSameElementsInOrderAs Seq(Example("test", 1), Example("test", 2)) 110 | } 111 | 112 | "convert a successful response body with json that doesn't validate into an exception" in { 113 | val response = HttpResponse( 114 | status = 200, 115 | json = Json.obj( 116 | "items" -> 117 | Json.arr( 118 | Json.obj("v1" -> "test"), 119 | Json.obj("v1" -> "test", "v2" -> 2) 120 | ) 121 | ), 122 | headers = Map.empty 123 | ) 124 | a[JsValidationException] should be thrownBy reads.read(exampleVerb, exampleUrl, response) 125 | } 126 | 127 | "convert a successful response body with json that is missing the given property into an exception" in { 128 | val response = HttpResponse( 129 | status = 200, 130 | json = Json.obj( 131 | "missing" -> 132 | Json.arr( 133 | Json.obj("v1" -> "test", "v2" -> 1), 134 | Json.obj("v1" -> "test", "v2" -> 2) 135 | ) 136 | ), 137 | headers = Map.empty 138 | ) 139 | a[JsValidationException] should be thrownBy reads.read(exampleVerb, exampleUrl, response) 140 | } 141 | 142 | "return None if the status code is 204 or 404" in { 143 | reads.read(exampleVerb, exampleUrl, exampleResponse(204)) should be(empty) 144 | reads.read(exampleVerb, exampleUrl, exampleResponse(404)) should be(empty) 145 | } 146 | 147 | "throw an Exception for any failure" in { 148 | forAll(Gen.posNum[Int].filter(!_.toString.startsWith("2"))) { s => 149 | an[Exception] should be thrownBy reads.read(exampleVerb, exampleUrl, exampleResponse(s)) 150 | } 151 | } 152 | } 153 | 154 | val exampleVerb = "GET" 155 | val exampleUrl = "http://example.com/something" 156 | def exampleResponse(statusCode: Int) = HttpResponse( 157 | status = statusCode, 158 | json = Json.parse("""{"test":1}"""), 159 | headers = Map("X-something" -> Seq("some value")), 160 | //responseString = Some("this is the string body") 161 | ) 162 | 163 | case class Example(v1: String, v2: Int) 164 | } 165 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/test/scala/uk/gov/hmrc/http/HttpResponseSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http 18 | 19 | import org.scalatest.wordspec.AnyWordSpec 20 | import org.scalatest.matchers.should.Matchers 21 | 22 | /** 23 | * Created by jonathan on 25/07/16. 24 | */ 25 | class HttpResponseSpec extends AnyWordSpec with Matchers { 26 | "unapply" should { 27 | "return matching object in" in { 28 | HttpResponse(status = 1, body = "test body", headers = Map("a" -> List("1", "2", "3"))) match { 29 | case HttpResponse(status, body, headers) => 30 | status shouldBe 1 31 | body shouldBe "test body" 32 | headers shouldBe Map("a" -> List("1", "2", "3")) 33 | } 34 | } 35 | } 36 | 37 | "header" should { 38 | "return the `headOption` value of the associated and case-insensitive header name" in { 39 | val headers = 40 | Map( 41 | "Test-Header-1" -> Vector("v1", "v2"), 42 | ) 43 | 44 | val response = 45 | HttpResponse( 46 | status = 200, 47 | body = "", 48 | headers = headers 49 | ) 50 | 51 | response.header("test-header-1") shouldBe Some("v1") 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/test/scala/uk/gov/hmrc/http/controllers/JsPathEnrichmentSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http.controllers 18 | 19 | import org.scalatest.wordspec.AnyWordSpecLike 20 | import org.scalatest.matchers.should.Matchers 21 | import play.api.libs.json._ 22 | 23 | @annotation.nowarn("msg=deprecated") 24 | class JsPathEnrichmentSpec extends AnyWordSpecLike with Matchers { 25 | 26 | import JsPathEnrichment.RichJsPath 27 | 28 | implicit val reads: Reads[Option[BigDecimal]] = (JsPath \ "rti" \ "balance").readNullable[BigDecimal] 29 | 30 | val pathDoesNotExistJson = 31 | Json.parse( 32 | """{ 33 | "nonRti": { 34 | "paidToDate": 200.25 35 | } 36 | }""" 37 | ) 38 | 39 | val pathExistsAndValueMissingJson = 40 | Json.parse( 41 | """{ 42 | "rti": { 43 | "notTheBalance": 123.45 44 | } 45 | }""" 46 | ) 47 | 48 | val pathAndValueExistsJson = 49 | Json.parse( 50 | """{ 51 | "rti": { 52 | "balance": 899.80 53 | } 54 | }""" 55 | ) 56 | 57 | "Parsing json when the path does not exist prior to the structure being parsed" should { 58 | "result in None without failure when early sections of path are not present" in { 59 | pathDoesNotExistJson.validate[Option[BigDecimal]] shouldBe JsSuccess(None) 60 | } 61 | 62 | "result in None without failure when the patch exists but the value does not" in { 63 | pathExistsAndValueMissingJson.validate[Option[BigDecimal]] shouldBe JsSuccess(None) 64 | } 65 | 66 | "result in value when path exists" in { 67 | pathAndValueExistsJson.validate[Option[BigDecimal]] shouldBe JsSuccess(Some(BigDecimal("899.80")), __ \ "rti" \ "balance") 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/test/scala/uk/gov/hmrc/http/logging/ConnectionTracingSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http.logging 18 | 19 | import org.mockito.Mockito.{reset, verify} 20 | import org.scalatest.BeforeAndAfterEach 21 | import org.scalatest.wordspec.AnyWordSpecLike 22 | import org.scalatest.matchers.should.Matchers 23 | import org.scalatestplus.mockito.MockitoSugar 24 | import org.slf4j.Logger 25 | import uk.gov.hmrc.http._ 26 | 27 | import scala.util.{Failure, Success} 28 | 29 | class ConnectionTracingSpec 30 | extends AnyWordSpecLike 31 | with Matchers 32 | with MockitoSugar 33 | with BeforeAndAfterEach { 34 | 35 | val mockPlayLogger = mock[Logger] 36 | 37 | val connectionTracing = new ConnectionTracing { 38 | override lazy val connectionLogger = mockPlayLogger 39 | } 40 | 41 | override def beforeEach() = 42 | reset(mockPlayLogger) 43 | 44 | "logResult" should { 45 | "log 200 as DEBUG" in { 46 | val ld = new StubLoggingDetails() 47 | 48 | val httpResult = Success("response") 49 | 50 | connectionTracing.logResult(ld, "GET", "/url", 1L)(httpResult) 51 | 52 | verify(mockPlayLogger).debug("RequestId(rId):GET:1:1ns:0:0ns:requestChain:/url:ok") 53 | } 54 | 55 | "log 404 error as INFO" in { 56 | val ld = new StubLoggingDetails() 57 | 58 | val httpResult = Failure(new NotFoundException("not found")) 59 | 60 | connectionTracing.logResult(ld, "GET", "/url", 1L)(httpResult) 61 | 62 | verify(mockPlayLogger).info("RequestId(rId):GET:1:1ns:0:0ns:requestChain:/url:failed not found") 63 | } 64 | 65 | "log 404 upstream error as INFO" in { 66 | val ld = new StubLoggingDetails() 67 | 68 | val httpResult = Failure(UpstreamErrorResponse(message = "404 error", statusCode = 404, reportAs = 404)) 69 | 70 | connectionTracing.logResult(ld, "GET", "/url", 1L)(httpResult) 71 | 72 | verify(mockPlayLogger).info("RequestId(rId):GET:1:1ns:0:0ns:requestChain:/url:failed 404 error") 73 | } 74 | 75 | "log 401 upstream error as WARN" in { 76 | val ld = new StubLoggingDetails() 77 | 78 | val httpResult = Failure(UpstreamErrorResponse(message = "401 error", statusCode = 401, reportAs = 401)) 79 | 80 | connectionTracing.logResult(ld, "GET", "/url", 1L)(httpResult) 81 | 82 | verify(mockPlayLogger).warn("RequestId(rId):GET:1:1ns:0:0ns:requestChain:/url:failed 401 error") 83 | } 84 | 85 | "log 400 error as WARN" in { 86 | val ld = new StubLoggingDetails() 87 | 88 | val httpResult = Failure(UpstreamErrorResponse(message = "400 error", statusCode = 400, reportAs = 400)) 89 | 90 | connectionTracing.logResult(ld, "GET", "/url", 1L)(httpResult) 91 | 92 | verify(mockPlayLogger).warn("RequestId(rId):GET:1:1ns:0:0ns:requestChain:/url:failed 400 error") 93 | } 94 | 95 | "log 500 upstream error as WARN" in { 96 | val ld = new StubLoggingDetails() 97 | 98 | val httpResult = Failure(UpstreamErrorResponse(message = "500 error", statusCode = 500, reportAs = 500)) 99 | 100 | connectionTracing.logResult(ld, "GET", "/url", 1L)(httpResult) 101 | 102 | verify(mockPlayLogger).warn("RequestId(rId):GET:1:1ns:0:0ns:requestChain:/url:failed 500 error") 103 | } 104 | 105 | "log 502 error as WARN" in { 106 | val ld = new StubLoggingDetails() 107 | 108 | val httpResult = Failure(new BadGatewayException("502 error")) 109 | 110 | connectionTracing.logResult(ld, "GET", "/url", 1L)(httpResult) 111 | 112 | verify(mockPlayLogger).warn("RequestId(rId):GET:1:1ns:0:0ns:requestChain:/url:failed 502 error") 113 | } 114 | 115 | "log unrecognised exception as WARN" in { 116 | val ld = new StubLoggingDetails() 117 | 118 | val httpResult = Failure(new Exception("unknown error")) 119 | 120 | connectionTracing.logResult(ld, "GET", "/url", 1L)(httpResult) 121 | 122 | verify(mockPlayLogger).warn("RequestId(rId):GET:1:1ns:0:0ns:requestChain:/url:failed unknown error") 123 | } 124 | } 125 | 126 | private class StubLoggingDetails extends LoggingDetails { 127 | import uk.gov.hmrc.http.{Authorization, ForwardedFor, RequestId, RequestChain, SessionId} 128 | 129 | override def sessionId: Option[SessionId] = Some(SessionId("sId")) 130 | 131 | override def forwarded: Option[ForwardedFor] = None 132 | 133 | override def requestId: Option[RequestId] = Some(RequestId("rId")) 134 | 135 | override def age: Long = 1L 136 | 137 | override def authorization: Option[Authorization] = None 138 | 139 | override def requestChain: RequestChain = new RequestChain("requestChain") 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/test/scala/uk/gov/hmrc/play/http/logging/MdcLoggingExecutionContextSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.play.http.logging 18 | 19 | import java.util.concurrent.{CountDownLatch, Executors} 20 | 21 | import ch.qos.logback.classic.spi.ILoggingEvent 22 | import ch.qos.logback.classic.{Level, Logger => LogbackLogger} 23 | import ch.qos.logback.core.AppenderBase 24 | import org.scalatest._ 25 | import org.scalatest.wordspec.AnyWordSpecLike 26 | import org.scalatest.matchers.should.Matchers 27 | import org.slf4j.{LoggerFactory, MDC} 28 | import play.core.NamedThreadFactory 29 | 30 | import scala.collection.mutable 31 | import scala.concurrent.duration._ 32 | import scala.concurrent.{Await, ExecutionContext, Future} 33 | import scala.reflect._ 34 | 35 | @annotation.nowarn("msg=deprecated") 36 | class MdcLoggingExecutionContextSpec 37 | extends AnyWordSpecLike 38 | with Matchers 39 | with LoneElement 40 | with Inspectors 41 | with BeforeAndAfter { 42 | 43 | before { 44 | MDC.clear() 45 | } 46 | 47 | "The MDC Transporting Execution Context" should { 48 | "capture the MDC map with values in it and put it in place when a task is run" in withCaptureOfLoggingFrom[MdcLoggingExecutionContextSpec] { logList => 49 | implicit val ec = createAndInitialiseMdcTransportingExecutionContext(Map(("someKey", "something"))) 50 | 51 | logEventInsideAFutureUsing(ec) 52 | 53 | logList.loneElement._2 should contain("someKey" -> "something") 54 | } 55 | 56 | "ignore an null MDC map" in withCaptureOfLoggingFrom[MdcLoggingExecutionContextSpec] { logList => 57 | implicit val ec = createAndInitialiseMdcTransportingExecutionContext(Map()) 58 | 59 | logEventInsideAFutureUsing(ec) 60 | 61 | logList.loneElement._2 should be(empty) 62 | } 63 | 64 | "clear the MDC map after a task is run" in withCaptureOfLoggingFrom[MdcLoggingExecutionContextSpec] { logList => 65 | implicit val ec = createAndInitialiseMdcTransportingExecutionContext(Map(("someKey", "something"))) 66 | 67 | doSomethingInsideAFutureButDontLog(ec) 68 | 69 | MDC.clear() 70 | logEventInsideAFutureUsing(ec) 71 | 72 | logList.loneElement._2 should be(Map("someKey" -> "something")) 73 | } 74 | 75 | "clear the MDC map after a task throws an exception" in withCaptureOfLoggingFrom[MdcLoggingExecutionContextSpec] { logList => 76 | implicit val ec = createAndInitialiseMdcTransportingExecutionContext(Map(("someKey", "something"))) 77 | 78 | throwAnExceptionInATaskOn(ec) 79 | 80 | MDC.clear() 81 | logEventInsideAFutureUsing(ec) 82 | 83 | logList.loneElement._2 should be(Map("someKey" -> "something")) 84 | } 85 | 86 | "log values from given MDC map when multiple threads are using it concurrently by ensuring each log from each thread has been logged via MDC" in withCaptureOfLoggingFrom[MdcLoggingExecutionContextSpec] { logList => 87 | val threadCount = 10 88 | val logCount = 10 89 | 90 | val concurrentThreadsEc = 91 | ExecutionContext.fromExecutor(Executors.newFixedThreadPool(threadCount, new NamedThreadFactory("LoggerThread"))) 92 | val startLatch = new CountDownLatch(threadCount) 93 | val completionLatch = new CountDownLatch(threadCount) 94 | 95 | for (t <- 0 until threadCount) { 96 | Future { 97 | MDC.clear() 98 | startLatch.countDown() 99 | startLatch.await() 100 | 101 | for (l <- 0 until logCount) { 102 | val mdc = Map("entry" -> s"${Thread.currentThread().getName}-$l") 103 | logEventInsideAFutureUsing(new MdcLoggingExecutionContext(ExecutionContext.global, mdc)) 104 | } 105 | 106 | completionLatch.countDown() 107 | }(concurrentThreadsEc) 108 | } 109 | 110 | completionLatch.await() 111 | 112 | val logs = logList.map(_._2).map(_.head._2).toSet 113 | logs.size should be(threadCount * logCount) 114 | 115 | for (t <- 1 until threadCount) { 116 | for (l <- 0 until logCount) { 117 | logs should contain(s"LoggerThread-$t-$l") 118 | } 119 | } 120 | } 121 | } 122 | 123 | def createAndInitialiseMdcTransportingExecutionContext(mdcData: Map[String, String]): MdcLoggingExecutionContext = { 124 | val ec = new MdcLoggingExecutionContext(ExecutionContext.fromExecutor(Executors.newFixedThreadPool(1)), mdcData) 125 | initialise(ec) 126 | ec 127 | } 128 | 129 | def logEventInsideAFutureUsingImplicitEc(implicit ec: ExecutionContext): Unit = 130 | logEventInsideAFutureUsing(ec) 131 | 132 | def logEventInsideAFutureUsing(ec: ExecutionContext): Unit = 133 | Await.ready( 134 | Future.apply( 135 | LoggerFactory.getLogger(classOf[MdcLoggingExecutionContextSpec]).info("") 136 | )(ec), 137 | 2.second 138 | ) 139 | 140 | def doSomethingInsideAFutureButDontLog(ec: ExecutionContext): Unit = 141 | Await.ready(Future.apply(())(ec), 2.second) 142 | 143 | def throwAnExceptionInATaskOn(ec: ExecutionContext): Unit = 144 | ec.execute(() => throw new RuntimeException("Test what happens when a task running on this EC throws an exception")) 145 | 146 | /** Ensures that a thread is already created in the execution context by running an empty future. 147 | * Required as otherwise the MDC is transferred to the new thread as it is stored in an inheritable 148 | * ThreadLocal. 149 | */ 150 | def initialise(ec: ExecutionContext): Unit = 151 | Await.ready(Future.apply(())(ec), 2.second) 152 | 153 | def withCaptureOfLoggingFrom[T: ClassTag](body: (=> List[(ILoggingEvent, Map[String, String])]) => Unit): Unit = { 154 | val logger = LoggerFactory.getLogger(classTag[T].runtimeClass).asInstanceOf[LogbackLogger] 155 | val appender = new InspectableAppender 156 | appender.setContext(logger.getLoggerContext) 157 | appender.start() 158 | logger.addAppender(appender) 159 | logger.setLevel(Level.ALL) 160 | logger.setAdditive(true) 161 | body(appender.list.toList) 162 | } 163 | } 164 | 165 | class InspectableAppender extends AppenderBase[ILoggingEvent]() { 166 | import scala.jdk.CollectionConverters._ 167 | 168 | val list = 169 | mutable.ListBuffer[(ILoggingEvent, Map[String, String])]() 170 | 171 | override def append(e: ILoggingEvent): Unit = 172 | list.append((e, e.getMDCPropertyMap.asScala.toMap)) 173 | } 174 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/test/scala/uk/gov/hmrc/play/http/logging/MdcSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.play.http.logging 18 | 19 | import org.apache.pekko.actor.ActorSystem 20 | import org.apache.pekko.dispatch.ExecutorServiceDelegate 21 | import org.apache.pekko.pattern.{after => scheduleAfter} 22 | import org.scalatest.BeforeAndAfter 23 | import org.scalatest.concurrent.{IntegrationPatience, ScalaFutures} 24 | import org.scalatest.matchers.should.Matchers 25 | import org.scalatest.wordspec.AnyWordSpecLike 26 | import org.slf4j.MDC 27 | 28 | import java.util.concurrent.{ExecutorService, Executors} 29 | import scala.concurrent.duration.DurationInt 30 | import scala.concurrent.{ExecutionContext, Future} 31 | 32 | class MdcSpec 33 | extends AnyWordSpecLike 34 | with Matchers 35 | with ScalaFutures 36 | with IntegrationPatience 37 | with BeforeAndAfter { 38 | 39 | after { 40 | // since we're initially adding MDC data on the test execution thread, we need to clean up to avoid affecting other tests 41 | MDC.clear() 42 | } 43 | 44 | "mdcData" should { 45 | "return a Scala Map" in { 46 | MDC.put("something1", "something2") 47 | Mdc.mdcData shouldBe Map("something1" -> "something2") 48 | } 49 | } 50 | 51 | "Preserving MDC" should { 52 | "show that MDC is lost when switching contexts" in { 53 | implicit val mdcEc: ExecutionContext = mdcPropagatingExecutionContext() 54 | 55 | (for { 56 | _ <- Future.successful(org.slf4j.MDC.put("k", "v")) 57 | _ <- runActionWhichLosesMdc() 58 | } yield 59 | Option(MDC.get("k")) 60 | ).futureValue shouldBe None 61 | } 62 | 63 | "restore MDC" in { 64 | implicit val mdcEc: ExecutionContext = mdcPropagatingExecutionContext() 65 | 66 | (for { 67 | _ <- Future.successful(org.slf4j.MDC.put("k", "v")) 68 | _ <- Mdc.preservingMdc(runActionWhichLosesMdc()) 69 | } yield 70 | Option(MDC.get("k")) 71 | ).futureValue shouldBe Some("v") 72 | } 73 | 74 | "restore MDC when exception is thrown" in { 75 | implicit val mdcEc: ExecutionContext = mdcPropagatingExecutionContext() 76 | 77 | (for { 78 | _ <- Future.successful(org.slf4j.MDC.put("k", "v")) 79 | _ <- Mdc.preservingMdc(runActionWhichLosesMdc(fail = true)) 80 | } yield None 81 | ).recover { case _ => 82 | Option(MDC.get("k")) 83 | }.futureValue shouldBe Some("v") 84 | } 85 | } 86 | 87 | private def runActionWhichLosesMdc(fail: Boolean = false): Future[Any] = { 88 | val as = ActorSystem("as") 89 | scheduleAfter(10.millis, as.scheduler)(Future(())(as.dispatcher))(as.dispatcher) 90 | .map(a => if (fail) sys.error("expected test exception") else a)(as.dispatcher) 91 | } 92 | 93 | private def mdcPropagatingExecutionContext() = 94 | ExecutionContext.fromExecutor(new MDCPropagatingExecutorService(Executors.newFixedThreadPool(2))) 95 | 96 | } 97 | 98 | // This class is copied from bootstrap-play. 99 | // There is a ticket in the backlog to consider extracting it neatly. For now, it is needed for this test. 100 | class MDCPropagatingExecutorService(val executor: ExecutorService) extends ExecutorServiceDelegate { 101 | 102 | override def execute(command: Runnable): Unit = { 103 | val mdcData = MDC.getCopyOfContextMap 104 | 105 | executor.execute(() => { 106 | val oldMdcData = MDC.getCopyOfContextMap 107 | setMDC(mdcData) 108 | try { 109 | command.run() 110 | } finally { 111 | // this means any Mdc updates on the ec will not be propagated once it steps out 112 | setMDC(oldMdcData) 113 | } 114 | }) 115 | } 116 | 117 | private def setMDC(context: java.util.Map[String, String]): Unit = 118 | if (context == null) 119 | MDC.clear() 120 | else 121 | MDC.setContextMap(context) 122 | } 123 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/test/scala/uk/gov/hmrc/play/test/PortFinder.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http.test 18 | 19 | import scala.util.Try 20 | 21 | import java.net.ServerSocket 22 | 23 | object PortFinder { 24 | 25 | def findFreePort(portRange: Range, excluded: Int*): Int = 26 | portRange 27 | .find(port => !excluded.contains(port) && isFree(port)) 28 | .getOrElse(throw new Exception("No free port")) 29 | 30 | private def isFree(port: Int): Boolean = { 31 | val triedSocket = Try { 32 | val serverSocket = new ServerSocket(port) 33 | Try(serverSocket.close()) 34 | serverSocket 35 | } 36 | triedSocket.isSuccess 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /http-verbs-play-30/src/test/scala/uk/gov/hmrc/play/test/WireMockSupport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http.test 18 | 19 | import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, Suite} 20 | import com.github.tomakehurst.wiremock._ 21 | import com.github.tomakehurst.wiremock.client.WireMock 22 | import com.github.tomakehurst.wiremock.core.WireMockConfiguration 23 | import org.slf4j.{Logger, LoggerFactory} 24 | 25 | trait WireMockSupport 26 | extends BeforeAndAfterAll 27 | with BeforeAndAfterEach { 28 | this: Suite => 29 | 30 | private val logger: Logger = LoggerFactory.getLogger(getClass) 31 | 32 | lazy val wireMockHost: String = 33 | "localhost" 34 | 35 | lazy val wireMockPort: Int = 36 | // we lookup a port ourselves rather than using `wireMockConfig().dynamicPort()` since it's simpler to provide 37 | // it up front (rather than query the running server), and allow overriding. 38 | PortFinder.findFreePort(portRange = 6001 to 7000) 39 | 40 | lazy val wireMockUrl: String = 41 | s"http://$wireMockHost:$wireMockPort" 42 | 43 | lazy val wireMockServer = 44 | new WireMockServer(WireMockConfiguration.wireMockConfig().port(wireMockPort)) 45 | 46 | override protected def beforeAll(): Unit = { 47 | super.beforeAll() 48 | wireMockServer.start() 49 | WireMock.configureFor(wireMockHost, wireMockServer.port()) 50 | logger.info(s"Started WireMock server on host: $wireMockHost, port: ${wireMockServer.port()}") 51 | } 52 | 53 | override protected def afterAll(): Unit = { 54 | wireMockServer.stop() 55 | super.afterAll() 56 | } 57 | 58 | override protected def beforeEach(): Unit = { 59 | super.beforeEach() 60 | wireMockServer.resetMappings() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /http-verbs-test-play-30/src/main/scala/uk/gov/hmrc/http/test/ExternalWireMockSupport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http.test 18 | 19 | import com.github.tomakehurst.wiremock.WireMockServer 20 | import com.github.tomakehurst.wiremock.client.WireMock 21 | import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig 22 | import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, Suite} 23 | import org.slf4j.{Logger, LoggerFactory} 24 | 25 | trait ExternalWireMockSupport extends BeforeAndAfterAll with BeforeAndAfterEach { 26 | this: Suite => 27 | 28 | private val logger: Logger = LoggerFactory.getLogger(getClass) 29 | 30 | lazy val externalWireMockHost: String = 31 | // "127.0.0.1" allows us to test locally, but is considered an external host by http-verbs since only "localhost" is 32 | // registered in `internalServiceHostPatterns` as an internal host. 33 | "127.0.0.1" 34 | 35 | lazy val externalWireMockPort: Int = 36 | // we lookup a port ourselves rather than using `wireMockConfig().dynamicPort()` since it's simpler to provide 37 | // it up front (rather than query the running server), and allow overriding. 38 | PortFinder.findFreePort(portRange = 6001 to 7000) 39 | 40 | lazy val externalWireMockRootDirectory: String = 41 | // wiremock doesn't look in the classpath, it uses src/test/resources by default. 42 | // since play projects use the non-standard `test/resources` we should attempt to identify the path 43 | // note, it may require `Test / fork := true` in sbt (or just override explicitly) 44 | System.getProperty("java.class.path").split(":").head 45 | 46 | lazy val externalWireMockServer: WireMockServer = 47 | new WireMockServer( 48 | wireMockConfig() 49 | .port(externalWireMockPort) 50 | .withRootDirectory(externalWireMockRootDirectory) 51 | ) 52 | 53 | lazy val externalWireMockUrl: String = 54 | s"http://$externalWireMockHost:$externalWireMockPort" 55 | 56 | /** If true (default) it will clear the wireMock settings before each test */ 57 | lazy val resetExternalWireMockMappings: Boolean = true 58 | 59 | def startExternalWireMock(): Unit = 60 | if (!externalWireMockServer.isRunning) { 61 | externalWireMockServer.start() 62 | // Note, if we use `ExternalWireMockSupport` in addition to `WireMockSupport`, then we can't use WireMock statically 63 | // since it's ambiguous. 64 | WireMock.configureFor(externalWireMockHost, externalWireMockServer.port()) 65 | logger.info(s"Started external WireMock server on host: $externalWireMockHost, port: ${externalWireMockServer.port()}, rootDirectory: $externalWireMockRootDirectory") 66 | } 67 | 68 | def stopExternalWireMock(): Unit = 69 | if (externalWireMockServer.isRunning) { 70 | externalWireMockServer.stop() 71 | logger.info(s"Stopped external WireMock server on host: $externalWireMockHost, port: $externalWireMockPort") 72 | } 73 | 74 | override protected def beforeAll(): Unit = { 75 | super.beforeAll() 76 | startExternalWireMock() 77 | } 78 | 79 | override protected def beforeEach(): Unit = { 80 | super.beforeEach() 81 | if (resetExternalWireMockMappings) 82 | externalWireMockServer.resetMappings() 83 | } 84 | 85 | override protected def afterAll(): Unit = { 86 | stopExternalWireMock() 87 | super.afterAll() 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /http-verbs-test-play-30/src/main/scala/uk/gov/hmrc/http/test/HttpClientSupport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http.test 18 | 19 | import com.typesafe.config.{Config, ConfigFactory} 20 | import org.apache.pekko.actor.ActorSystem 21 | import play.api.libs.ws.WSClient 22 | import play.api.libs.ws.ahc.{AhcWSClient, AhcWSClientConfigFactory} 23 | import uk.gov.hmrc.http.HttpClient 24 | import uk.gov.hmrc.http.hooks.HttpHook 25 | import uk.gov.hmrc.play.http.ws.WSHttp 26 | 27 | @annotation.nowarn("msg=deprecated") 28 | trait HttpClientSupport { 29 | def mkHttpClient( 30 | config: Config = ConfigFactory.load() 31 | ) = 32 | new HttpClient with WSHttp { 33 | private implicit val as: ActorSystem = ActorSystem("test-actor-system") 34 | 35 | override val wsClient: WSClient = AhcWSClient(AhcWSClientConfigFactory.forConfig(config)) 36 | override protected val configuration: Config = config 37 | override val hooks: Seq[HttpHook] = Seq.empty 38 | override protected def actorSystem: ActorSystem = as 39 | } 40 | 41 | lazy val httpClient: HttpClient = mkHttpClient() 42 | } 43 | -------------------------------------------------------------------------------- /http-verbs-test-play-30/src/main/scala/uk/gov/hmrc/http/test/HttpClientV2Support.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http.test 18 | 19 | import com.typesafe.config.ConfigFactory 20 | import org.apache.pekko.actor.ActorSystem 21 | import play.api.Configuration 22 | import play.api.libs.ws.ahc.{AhcWSClient, AhcWSClientConfigFactory} 23 | import uk.gov.hmrc.http.client.{HttpClientV2, HttpClientV2Impl} 24 | 25 | trait HttpClientV2Support { 26 | 27 | def mkHttpClientV2( 28 | config: Configuration = Configuration(ConfigFactory.load()) 29 | ): HttpClientV2 = { 30 | implicit val as: ActorSystem = ActorSystem("test-actor-system") 31 | 32 | new HttpClientV2Impl( 33 | wsClient = AhcWSClient(AhcWSClientConfigFactory.forConfig(config.underlying)), 34 | as, 35 | config, 36 | hooks = Seq.empty, 37 | ) 38 | } 39 | 40 | lazy val httpClientV2: HttpClientV2 = mkHttpClientV2() 41 | } 42 | -------------------------------------------------------------------------------- /http-verbs-test-play-30/src/main/scala/uk/gov/hmrc/http/test/PortFinder.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http.test 18 | 19 | import java.net.ServerSocket 20 | 21 | import scala.util.Try 22 | 23 | object PortFinder { 24 | 25 | def findFreePort(portRange: Range, excluded: Int*): Int = 26 | portRange 27 | .find(port => !excluded.contains(port) && isFree(port)) 28 | .getOrElse(throw new Exception("No free port")) 29 | 30 | private def isFree(port: Int): Boolean = { 31 | val triedSocket = Try { 32 | val serverSocket = new ServerSocket(port) 33 | Try(serverSocket.close()) 34 | serverSocket 35 | } 36 | triedSocket.isSuccess 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /http-verbs-test-play-30/src/main/scala/uk/gov/hmrc/http/test/WireMockSupport.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http.test 18 | 19 | import com.github.tomakehurst.wiremock.WireMockServer 20 | import com.github.tomakehurst.wiremock.client.WireMock 21 | import com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig 22 | import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, Suite} 23 | import org.slf4j.{Logger, LoggerFactory} 24 | 25 | trait WireMockSupport extends BeforeAndAfterAll with BeforeAndAfterEach { 26 | this: Suite => 27 | 28 | private val logger: Logger = LoggerFactory.getLogger(getClass) 29 | 30 | lazy val wireMockHost: String = 31 | // this has to match the configuration in `internalServiceHostPatterns` 32 | "localhost" 33 | 34 | lazy val wireMockPort: Int = 35 | // we lookup a port ourselves rather than using `wireMockConfig().dynamicPort()` since it's simpler to provide 36 | // it up front (rather than query the running server), and allow overriding. 37 | PortFinder.findFreePort(portRange = 6001 to 7000) 38 | 39 | lazy val wireMockRootDirectory: String = 40 | // wiremock doesn't look in the classpath, it uses src/test/resources by default. 41 | // since play projects use the non-standard `test/resources` we should attempt to identify the path 42 | // note, it may require `Test / fork := true` in sbt (or just override explicitly) 43 | System.getProperty("java.class.path").split(":").head 44 | 45 | lazy val wireMockServer: WireMockServer = 46 | new WireMockServer( 47 | wireMockConfig() 48 | .port(wireMockPort) 49 | .withRootDirectory(wireMockRootDirectory) 50 | ) 51 | 52 | lazy val wireMockUrl: String = 53 | s"http://$wireMockHost:$wireMockPort" 54 | 55 | /** If true (default) it will clear the wireMock settings before each test */ 56 | lazy val resetWireMockMappings: Boolean = true 57 | lazy val resetWireMockRequests: Boolean = true 58 | 59 | def startWireMock(): Unit = 60 | if (!wireMockServer.isRunning) { 61 | wireMockServer.start() 62 | // this initialises static access to `WireMock` rather than calling functions on the wireMockServer instance itself 63 | WireMock.configureFor(wireMockHost, wireMockServer.port()) 64 | logger.info(s"Started WireMock server on host: $wireMockHost, port: ${wireMockServer.port()}, rootDirectory: $wireMockRootDirectory") 65 | } 66 | 67 | def stopWireMock(): Unit = 68 | if (wireMockServer.isRunning) { 69 | wireMockServer.stop() 70 | logger.info(s"Stopped WireMock server on host: $wireMockHost, port: $wireMockPort") 71 | } 72 | 73 | override protected def beforeAll(): Unit = { 74 | super.beforeAll() 75 | startWireMock() 76 | } 77 | 78 | override protected def beforeEach(): Unit = { 79 | super.beforeEach() 80 | if (resetWireMockMappings) 81 | wireMockServer.resetMappings() 82 | if (resetWireMockRequests) 83 | wireMockServer.resetRequests() 84 | } 85 | 86 | override protected def afterAll(): Unit = { 87 | stopWireMock() 88 | super.afterAll() 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /http-verbs-test-play-30/src/main/scala/uk/gov/hmrc/play/http/test/ResponseMatchers.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.play.http.test 18 | 19 | import org.scalatest.concurrent.{IntegrationPatience, ScalaFutures} 20 | import org.scalatest.matchers.{HavePropertyMatchResult, HavePropertyMatcher} 21 | import play.api.libs.json._ 22 | import play.api.libs.ws.WSResponse 23 | 24 | import scala.concurrent.Future 25 | 26 | trait ResponseMatchers extends ScalaFutures with IntegrationPatience { 27 | 28 | /** 29 | * Enables syntax like: 30 | * resource("/write/audit").post(validAuditRequest) should have (status (204)) 31 | */ 32 | def status(expected: Int) = new HavePropertyMatcher[Future[WSResponse], Int] { 33 | def apply(response: Future[WSResponse]) = HavePropertyMatchResult( 34 | matches = response.futureValue.status == expected, 35 | propertyName = "Response HTTP Status", 36 | expectedValue = expected, 37 | actualValue = response.futureValue.status 38 | ) 39 | } 40 | 41 | /** 42 | * Enables syntax like: 43 | * resource("/write/audit").post(validAuditRequest) should have (body ("Invalid nino: !@£$%^&*^")) 44 | */ 45 | def body(expected: String) = new HavePropertyMatcher[Future[WSResponse], String] { 46 | def apply(response: Future[WSResponse]) = HavePropertyMatchResult( 47 | matches = response.futureValue.body == expected, 48 | propertyName = "Response Body", 49 | expectedValue = expected, 50 | actualValue = response.futureValue.body 51 | ) 52 | } 53 | 54 | /** 55 | * Enables syntax like: 56 | * resource("/write/audit").post(validAuditRequest) should have (jsonContent ("""{ "valid": true }""")) 57 | */ 58 | def jsonContent(expected: String) = new HavePropertyMatcher[Future[WSResponse], JsValue] { 59 | val expectedAsJson = Json.parse(expected) 60 | 61 | def apply(response: Future[WSResponse]) = { 62 | HavePropertyMatchResult( 63 | matches = response.futureValue.json == expectedAsJson, 64 | propertyName = "Response Content JSON", 65 | expectedValue = expectedAsJson, 66 | actualValue = response.futureValue.json 67 | ) 68 | } 69 | } 70 | 71 | /** 72 | * Checks if a property is defined and has a specific value 73 | * Enables syntax like: 74 | * resource("/write/audit").post(validAuditRequest) should have (jsonProperty (__ \ "valid", true)) 75 | */ 76 | def jsonProperty[E](path: JsPath, expected: E)(implicit eReads: Reads[E]) = new HavePropertyMatcher[Future[WSResponse], String] { 77 | def apply(response: Future[WSResponse]) = HavePropertyMatchResult( 78 | matches = response.futureValue.json.validate(path.read[E]).map(_ == expected).getOrElse(false), 79 | propertyName = "Response JSON at path " + path, 80 | expectedValue = expected.toString, 81 | actualValue = { 82 | val json = response.futureValue.json 83 | json.validate(path.read[E]).map(_.toString).getOrElse(json.toString) 84 | } 85 | ) 86 | } 87 | 88 | /** 89 | * Checks if a property is defined 90 | * Enables syntax like: 91 | * resource("/write/audit").post(validAuditRequest) should have (jsonProperty (__ \ "valid")) 92 | */ 93 | def jsonProperty(path: JsPath) = new HavePropertyMatcher[Future[WSResponse], JsValue] { 94 | def apply(response: Future[WSResponse]) = HavePropertyMatchResult( 95 | matches = response.futureValue.json.validate(path.readNullable[JsValue]).get.isDefined, 96 | propertyName = "Response JSON at path " + path, 97 | expectedValue = JsString("defined"), 98 | actualValue = response.futureValue.json.validate(path.readNullable[JsValue]).get.getOrElse(JsNull) 99 | ) 100 | } 101 | } 102 | 103 | object ResponseMatchers extends ResponseMatchers 104 | -------------------------------------------------------------------------------- /http-verbs-test-play-30/src/test/resources/__files/bankHolidays.json: -------------------------------------------------------------------------------- 1 | { 2 | "events": [ 3 | { 4 | "title": "New Year's Day", 5 | "date": "2017-01-02" 6 | }, 7 | { 8 | "title": "Good Friday", 9 | "date": "2017-04-14" 10 | }, 11 | { 12 | "title": "Easter Monday", 13 | "date": "2017-04-17" 14 | }, 15 | { 16 | "title": "Early May bank holiday", 17 | "date": "2017-05-01" 18 | }, 19 | { 20 | "title": "Spring bank holiday", 21 | "date": "2017-05-29" 22 | }, 23 | { 24 | "title": "Summer bank holiday", 25 | "date": "2017-08-28" 26 | }, 27 | { 28 | "title": "Christmas Day", 29 | "date": "2017-12-25" 30 | }, 31 | { 32 | "title": "Boxing Day", 33 | "date": "2017-12-26" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /http-verbs-test-play-30/src/test/resources/__files/bankHolidays.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | New Year's Day 5 | 2017-01-02 6 | 7 | 8 | Good Friday 9 | 2017-04-14 10 | 11 | 12 | Easter Monday 13 | 2017-04-17 14 | 15 | 16 | Early May bank holiday 17 | 2017-05-01 18 | 19 | 20 | Spring bank holiday 21 | 2017-05-29 22 | 23 | 24 | Summer bank holiday 25 | 2017-08-28 26 | 27 | 28 | Christmas Day 29 | 2017-12-25 30 | 31 | 32 | Boxing Day 33 | 2017-12-26 34 | 35 | 36 | -------------------------------------------------------------------------------- /http-verbs-test-play-30/src/test/resources/__files/userId.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "123" 3 | } -------------------------------------------------------------------------------- /http-verbs-test-play-30/src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | %date [%level] [%logger] [%thread] %message%n 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /http-verbs-test-play-30/src/test/scala/uk/gov/hmrc/http/test/SupportSpec.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 HM Revenue & Customs 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package uk.gov.hmrc.http.test 18 | 19 | import com.github.tomakehurst.wiremock.client.WireMock._ 20 | import org.scalatest.concurrent.{IntegrationPatience, ScalaFutures} 21 | import org.scalatest.matchers.should.Matchers 22 | import org.scalatest.wordspec.AnyWordSpec 23 | import uk.gov.hmrc.http._ 24 | import uk.gov.hmrc.http.HttpReads.Implicits._ 25 | 26 | import scala.concurrent.ExecutionContext.Implicits.global 27 | 28 | class SupportSpec 29 | extends AnyWordSpec 30 | with Matchers 31 | with ScalaFutures 32 | with IntegrationPatience 33 | with HttpClientSupport 34 | with WireMockSupport 35 | with ExternalWireMockSupport { 36 | 37 | "WireMockSupport" should { 38 | "allow the user to simulate internal calls" in { 39 | implicit val hc: HeaderCarrier = HeaderCarrier(authorization = Some(Authorization("Basic dXNlcjoxMjM="))) 40 | 41 | wireMockServer.stubFor( 42 | post(urlEqualTo("/create-user")) 43 | .willReturn(aResponse().withStatus(200)) 44 | ) 45 | 46 | // auth header is forwarded on for internal calls 47 | httpClient.POST[String, HttpResponse]( 48 | url = url"$wireMockUrl/create-user", 49 | body = "body", 50 | headers = Seq.empty 51 | ).futureValue.status shouldBe 200 52 | 53 | wireMockServer.verify( 54 | postRequestedFor(urlEqualTo("/create-user")) 55 | .withHeader("Authorization", equalTo("Basic dXNlcjoxMjM=")) 56 | ) 57 | } 58 | } 59 | 60 | "ExternalWireMockSupport" should { 61 | "allow the user to simulate external calls" in { 62 | implicit val hc: HeaderCarrier = HeaderCarrier(authorization = Some(Authorization("Basic dXNlcjoxMjM="))) 63 | 64 | externalWireMockServer.stubFor( 65 | post(urlEqualTo("/create-user")) 66 | .willReturn(aResponse().withStatus(200)) 67 | ) 68 | 69 | // auth header is *not* forwarded on for external calls 70 | httpClient.POST[String, HttpResponse]( 71 | url = url"$externalWireMockUrl/create-user", 72 | body = "body", 73 | headers = Seq.empty 74 | ).futureValue.status shouldBe 200 75 | 76 | externalWireMockServer.verify( 77 | postRequestedFor(urlEqualTo("/create-user")) 78 | .withoutHeader("Authorization") 79 | ) 80 | 81 | // auth header can be forwarded explicitly 82 | httpClient.POST[String, HttpResponse]( 83 | url = url"$externalWireMockUrl/create-user", 84 | body = "body", 85 | headers = hc.headers(Seq(hc.names.authorisation)) 86 | ).futureValue.status shouldBe 200 87 | 88 | externalWireMockServer.verify( 89 | postRequestedFor(urlEqualTo("/create-user")) 90 | .withHeader("Authorization", equalTo("Basic dXNlcjoxMjM=")) 91 | ) 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /project/CopySources.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import sbt.Keys._ 3 | 4 | object CopySources { 5 | /** Copies source files from one module to another, and applies transformations */ 6 | def copySources(module: Project, transformSource: String => String, transformResource: String => String) = { 7 | def transformWith(fromSetting: SettingKey[File], toSetting: SettingKey[File], transform: String => String) = 8 | Def.task { 9 | val from = fromSetting.value 10 | val to = toSetting.value 11 | val files = (from ** "*").get.filterNot(_.isDirectory) 12 | println(s"Copying and transforming the following files for ${moduleName.value} scalaVersion ${scalaVersion.value}: files:\n${files.map(" " + _).mkString("\n")}}") 13 | files.map { file => 14 | val targetFile = new java.io.File(file.getParent.replace(from.getPath, to.getPath)) / file.getName 15 | IO.write(targetFile, transform(IO.read(file))) 16 | targetFile 17 | } 18 | } 19 | 20 | def include(location: File) = { 21 | val files = (location ** "*").get.filterNot(_.isDirectory) 22 | files.map(file => file -> file.getPath.stripPrefix(location.getPath + "/")) 23 | } 24 | 25 | Seq( 26 | // we really need `unmanagedSourceDirectories` rather than `scalaSource` to include scala-2.13, scala-2, scala-3 etc. 27 | // but it gets complicated when the target module will support different ones 28 | // for now, duplicate scala specific code in each module as required 29 | Compile / sourceGenerators += transformWith(module / Compile / scalaSource , Compile / sourceManaged , transformSource ).taskValue, 30 | Compile / resourceGenerators += transformWith(module / Compile / resourceDirectory, Compile / resourceManaged, transformResource).taskValue, 31 | Test / sourceGenerators += transformWith(module / Test / scalaSource , Test / sourceManaged , transformSource ).taskValue, 32 | Test / resourceGenerators += transformWith(module / Test / resourceDirectory, Test / resourceManaged, transformResource).taskValue, 33 | // generated sources are not included in source.jar by default 34 | Compile / packageSrc / mappings ++= include((Compile / sourceManaged).value) ++ 35 | include((Compile / resourceManaged).value) 36 | ) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /project/LibDependencies.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | 3 | object LibDependencies { 4 | 5 | val play29Version = "2.9.6" 6 | val play30Version = "3.0.6" 7 | 8 | // Dependencies for http-verbs-common and http-verbs-play-xxx modules 9 | def coreCompileCommon(scalaVersion: String) = Seq( 10 | "com.typesafe" % "config" % "1.4.3", 11 | "com.softwaremill.sttp.model" %% "core" % "1.7.10", 12 | "dev.zio" %% "izumi-reflect" % "2.3.8" 13 | ) 14 | 15 | val coreCompilePlay29 = Seq( 16 | "com.typesafe.play" %% "play-json" % "2.10.6", // version provided by play29Version 17 | "org.slf4j" % "slf4j-api" % "2.0.9", 18 | "com.typesafe.play" %% "play-ahc-ws" % play29Version 19 | ) 20 | 21 | val coreCompilePlay30 = Seq( 22 | "org.playframework" %% "play-json" % "3.0.4", // version provided by play30Version 23 | "org.slf4j" % "slf4j-api" % "2.0.9", 24 | "org.playframework" %% "play-ahc-ws" % play30Version 25 | ) 26 | 27 | def coreTestCommon = Seq( 28 | "org.scalatest" %% "scalatest" % "3.2.17" % Test, 29 | "org.scalatestplus" %% "scalacheck-1-17" % "3.2.17.0" % Test, 30 | "com.vladsch.flexmark" % "flexmark-all" % "0.64.8" % Test, 31 | "org.scalatestplus" %% "mockito-4-11" % "3.2.17.0" % Test 32 | ) 33 | 34 | val coreTestPlay29 = Seq( 35 | "com.typesafe.play" %% "play-test" % play29Version % Test, 36 | "ch.qos.logback" % "logback-classic" % "1.4.11" % Test, // should already provided by play-test, why does it fail without it? 37 | "com.github.tomakehurst" % "wiremock" % "3.0.0-beta-7" % Test, 38 | "org.slf4j" % "slf4j-simple" % "2.0.7" % Test 39 | ) 40 | 41 | val coreTestPlay30 = Seq( 42 | "org.playframework" %% "play-test" % play30Version % Test, 43 | "ch.qos.logback" % "logback-classic" % "1.4.11" % Test, // should already provided by play-test, why does it fail without it? 44 | "com.github.tomakehurst" % "wiremock" % "3.0.0-beta-7" % Test, 45 | "org.slf4j" % "slf4j-simple" % "2.0.7" % Test 46 | ) 47 | 48 | val testCompilePlay29 = Seq( 49 | "org.scalatest" %% "scalatest" % "3.2.17", // version provided transitively is chosen for compatibility with scalatestplus-play 50 | "com.github.tomakehurst" % "wiremock" % "3.0.0-beta-7", // last version with jackson dependencies compatible with play 51 | "com.vladsch.flexmark" % "flexmark-all" % "0.64.8" % Test 52 | ) 53 | 54 | val testCompilePlay30 = Seq( 55 | "org.scalatest" %% "scalatest" % "3.2.17", // version provided transitively is chosen for compatibility with scalatestplus-play 56 | "com.github.tomakehurst" % "wiremock" % "3.0.0-beta-7", // last version with jackson dependencies compatible with play 57 | "com.vladsch.flexmark" % "flexmark-all" % "0.64.8" % Test 58 | ) 59 | } 60 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2018 HM Revenue & Customs 3 | # 4 | # Licensed under the Apache License, Version 2.0 (the "License"); 5 | # you may not use this file except in compliance with the License. 6 | # You may obtain a copy of the License at 7 | # 8 | # http://www.apache.org/licenses/LICENSE-2.0 9 | # 10 | # Unless required by applicable law or agreed to in writing, software 11 | # distributed under the License is distributed on an "AS IS" BASIS, 12 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | # See the License for the specific language governing permissions and 14 | # limitations under the License. 15 | # 16 | 17 | sbt.version=1.9.9 18 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += MavenRepository("HMRC-open-artefacts-maven2", "https://open.artefacts.tax.service.gov.uk/maven2") 2 | resolvers += Resolver.url("HMRC-open-artefacts-ivy2", url("https://open.artefacts.tax.service.gov.uk/ivy2"))(Resolver.ivyStylePatterns) 3 | 4 | addSbtPlugin("uk.gov.hmrc" % "sbt-auto-build" % "3.24.0") 5 | -------------------------------------------------------------------------------- /repository.yaml: -------------------------------------------------------------------------------- 1 | repoVisibility: public_0C3F0CE3E6E6448FAD341E7BFA50FCD333E06A20CFF05FCACE61154DDBBADF71 2 | type: library 3 | description: Library which encapsulates common concerns for calling other HTTP services 4 | --------------------------------------------------------------------------------