Search code examples
scalasprayspray-clientucip

How do I set a non-standard User-Agent using spray-client?


I'm building an application for a Telco, using Scala and Akka, and need to communicate with Account Information and Refill servers using the UCIP protocol.

UCIP is a simple protocol, built on XMLRPC; the only issue I'm having is that it requires clients to set the User-Agent header in the specific format User-Agent: <client name>/<protocol version>/<client version>, which spray parses as invalid.

I tried creating a custom User-Agent header, inheriting from spray.http.HttpHeader but it still doesn't work. Here's what I've got so far:

import akka.actor.ActorSystem
import akka.event.{Logging, LoggingAdapter}
import spray.client.pipelining._
import spray.http._
import spray.httpx._

case class `User-Agent`(value: String) extends HttpHeader {
    def lowercaseName: String = "user-agent"
    def name: String = "User-Agent"
    def render[R <: Rendering](r: R): r.type = r ~~ s"User-Agent: $value"
}

class UcipClient(val url: String, val protocol: String, username: String, password: String) (implicit system: ActorSystem) {

    val log = Logging.getLogger(system, this)
    val logRequest: HttpRequest => HttpRequest = { r => log.debug(r.toString); r }
    val logResponse: HttpResponse => HttpResponse = { r => log.debug(r.toString); r }

    val pipeline = (
        addHeader(`User-Agent`("USSD-UCIP/%s/1.0".format(protocol)))
        ~> addCredentials(BasicHttpCredentials(username, password))
        ~> logRequest
        ~> sendReceive
        ~> logResponse
    )

    def send(req: UcipRequest) = pipeline(Post(url, req.getRequest))
}

My requests keep returning "Sorry, Error occured: 403, Invalid protocol version Not defined", however, they return the correct response when I send the same details using curl.

What am I missing, and is this even possible with spray-client? I've spent a fair bit of time checking the internets (which led me towards the custom header route), but still haven't figured this out...would really appreciate any help :-)


Solution

  • Turns out I wasn't far from the answer. While examining the headers being sent over the wire, I noticed the User-Agent was being set twice: once by my code, and again by Spray (because it considered my header invalid).

    Setting the spray.can.client.user-agent-header to the empty string "" removed the second header, and requests were successful. Here's the final version of the custom header:

    import spray.http._
    
    object CustomHttpHeaders {
        case class `User-Agent`(val value: String) extends HttpHeader with Product with Serializable {
            def lowercaseName: String = "user-agent"
            def name: String = "User-Agent"
            def render[R <: Rendering](r: R): r.type = r ~~ s"User-Agent: $value"
        }
    }
    

    And the final UCIP client:

    import akka.actor.ActorRefFactory
    import com.typesafe.config.Config
    import scala.concurrent.ExecutionContext.Implicits.global
    import scala.xml.NodeSeq
    import spray.client.pipelining._
    import spray.http._
    import spray.httpx._
    
    class UcipFault(val code: Int, msg: String) extends RuntimeException(s"$code: $msg")
    
    class AirException(val code: Int) extends RuntimeException(s"$code")
    
    class UcipClient(config: Config, val url: String)(implicit context: ActorRefFactory) {
        import CustomHttpHeaders._
    
        val throwOnFailure: NodeSeq => NodeSeq = {
            case f if (f \\ "fault").size != 0 =>
                val faultData = (f \\ "fault" \\ "member" \ "value")
                throw new UcipFault((faultData \\ "i4").text.toInt,
                                    (faultData \\ "string").text)
            case el =>
                val responseCode = ((el \\ "member")
                    .filter { n => (n \\ "name").text == "responseCode" }
                    .map { n => (n \\ "i4").text.toInt }).head
                if (responseCode == 0) el else throw new AirException(responseCode)
        }
    
        val pipeline = (
            addHeader(`User-Agent`("USSD-UCIP/%s/1.0".format(config.getString("ucip.server-protocol"))))
            ~> addCredentials(BasicHttpCredentials(config.getString("ucip.server-username"), config.getString("ucip.server-password")))
            ~> sendReceive
            ~> unmarshal[NodeSeq]
            ~> throwOnFailure
        )
    
        def send(req: UcipRequest) = pipeline(Post(url, req.getRequest))
    }