es0329
es0329

Reputation: 1432

Why is this OkHttp POST that supports a BODY missing its Content-Type Header?

I saw that Content-Type header is removed for methods that don't support a Body, but that isn't my case. I've also confirmed my User-Agent header is successfully set.

This can be done statically via the interface with the endpoint's definition but I'd favor a global Interceptor over annotating all my methods.

// Api.kt
@POST("authenticated_users")
fun postUser(
    @Body newUser: NewUser
): Observable<AuthUser>

class UserRepo @Inject constructor(private val api: Api) {
    fun postUser(newUser: NewUser) = api.postUser(newUser)
}

// NetModule.kt
@Provides @Singleton
fun providesOkHttpClient(cache: Cache, app: Application): OkHttpClient {
    val timeoutInSeconds = 90.toLong()
    val builder = OkHttpClient.Builder()
        .cache(cache)
        .addInterceptor(MyInterceptor(app))
        .connectTimeout(timeoutInSeconds, TimeUnit.SECONDS)
        .readTimeout(timeoutInSeconds, TimeUnit.SECONDS)

    when {
        BuildConfig.DEBUG -> {
            val loggingInterceptor = HttpLoggingInterceptor().apply {
                level = HttpLoggingInterceptor.Level.HEADERS
            }

            with(builder) {
                addInterceptor(loggingInterceptor)
                addNetworkInterceptor(StethoInterceptor())
            }
        }
    }

    return builder.build()
}

@Provides @Singleton
fun providesMoshi(): Moshi {
    val jsonApiAdapterFactory = ResourceAdapterFactory.builder()
        .add(TermsConditions::class.java)
        .add(AuthUser::class.java)
        .add(Unknown::class.java)
        .build()

    val builder = Moshi.Builder()
        .add(jsonApiAdapterFactory)
        .add(KotlinJsonAdapterFactory())
    return builder.build()
}

@Provides @Singleton
fun providesRetrofit(okHttpClient: OkHttpClient, moshi: Moshi): Retrofit {
    return Retrofit.Builder()
        // .addConverterFactory(ScalarsConverterFactory.create())
        .addConverterFactory(JsonApiConverterFactory.create(moshi))
        .addConverterFactory(MoshiConverterFactory.create(moshi))
        .addCallAdapterFactory(CoroutineCallAdapterFactory())
        .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
        .baseUrl(baseUrl)
        .client(okHttpClient)
        .build()
}

// MyInterceptor.kt
class MyInterceptor @Inject constructor(private val app: Application) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val initialRequest = chain.request()
        val finalRequest = setHeaders(initialRequest)
        return chain.proceed(finalRequest)
    }

    private fun setHeaders(initialRequest: Request): Request {
        return initialRequest.newBuilder()
            // .header("Content-Type", "application/vnd.api+json")
            .header("User-Agent", "MyApp v${BuildConfig.VERSION_NAME}")
            .build()
    }
}

// MyViewModel.kt
fun createUser() {
    userObserver = object : DisposableObserver<AuthUser>() {
        override fun onNext(authUser: AuthUser) {
            statusData.postValue(true)
        }

        override fun onError(e: Throwable) {
            Timber.w(e.localizedMessage)
            error.postValue(e.localizedMessage)
        }

        override fun onComplete() {
            // no-op
        }
    }

    userRepo.postUser(newUser)
        .subscribeOn(Schedulers.newThread())
        .observeOn(AndroidSchedulers.mainThread())
        .subscribe(userObserver)
}

// log1.txt Retrofit with ScalarsConverterFactory
2018-04-18 15:20:35.772 16491-17436/com.es0329.myapp D/OkHttp: --> POST https://api.es0329.com/v5/authenticated_users
    Content-Type: text/plain; charset=UTF-8
    Content-Length: 259
    User-Agent: MyApp v1.5.1
    --> END POST
2018-04-18 15:20:36.278 16491-17436/com.es0329.myapp D/OkHttp: <-- 500 https://api.es0329.com/v5/authenticated_users (505ms)

// log2.txt Retrofit without ScalarsConverterFactory
2018-04-18 18:25:45.742 5017-6325/com.es0329.myapp D/OkHttp: --> POST https://api.es0329.com/v5/authenticated_users
    Content-Type: application/json; charset=UTF-8
    Content-Length: 311
    User-Agent: MyApp v1.5.1
    --> END POST
2018-04-18 18:25:45.868 5017-6325/com.es0329.myapp D/OkHttp: <-- 500 https://api.es0329.com/v5/authenticated_users (125ms)

// log3.txt after modifying JsonApiConverterFactory's `MediaType`
2018-04-18 20:35:47.322 19368-19931/com.es0329.myapp D/OkHttp: --> POST https://api.es0329.com/v5/authenticated_users
    Content-Type: application/vnd.api+json
    Content-Length: 268
    User-Agent: MyApp v1.5.1
    --> END POST
2018-04-18 20:35:49.058 19368-19931/com.es0329.myapp D/OkHttp: <-- 200 https://api.es0329.com/v5/authenticated_users (1735ms)

Upvotes: 0

Views: 3300

Answers (1)

Eugen Pechanec
Eugen Pechanec

Reputation: 38243

Why is it not working

Retrofit is in charge of setting appropriate content type and length based on registered converters and what you provide to your @Body parameter.

In greater detail: A Retrofit converter is responsible for transforming the type of your @Body to okhttp3.RequestBody which holds your content bytes, content length, and content type. Similarly on the way back. You supply content, ResponseBody handles details like HTTP headers.

You can't manually override these headers.

As you can see in the log, your string body gets successfully transmitted as text/plain.

--> POST https://api.es0329.com/v5/authenticated_users
Content-Type: text/plain; charset=UTF-8
Content-Length: 259
User-Agent: MyApp v1.5.1
--> END POST

That leads me to believe you have a registered converter and it's the scalar converter, which states:

A Converter which supports converting strings and both primitives and their boxed types to text/plain bodies.

What to to instead

All of the ready-made converters (Moshi, Gson, Jackson) are built to convert POJOs to application/json. This is a typical case and you should use one of these if you can. Explore source code here.

There are plenty of tutorials online for this case.

Rocky alternative

If for some reason you want/need to continue your current direction, that is prepare a JSON string manually and send that as application/vnd.api+json, you'll need a custom converter.

The aforementioned scalar converter already knows how to transform strings, so copy it into your project and adapt it to your needs (change the mime type). It's just a set of three classes:

  • convertor factory
  • request body convertor (transforms the @Body to okhttp3.RequestBody)
  • repsonse body convertor (transforms the okhttp3.ResponseBody to return value)

Upvotes: 1

Related Questions