Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle Empty Body #1554

Closed
roybrener opened this issue Feb 1, 2016 · 42 comments
Closed

Handle Empty Body #1554

roybrener opened this issue Feb 1, 2016 · 42 comments

Comments

@roybrener
Copy link

After updating to retrofit beta-3 I'm getting the Exception (Because of an empty body)

java.io.EOFException: End of input at line 1 column 1
                                                                           at com.google.gson.stream.JsonReader.nextNonWhitespace(JsonReader.java:1414)
                                                                           at com.google.gson.stream.JsonReader.doPeek(JsonReader.java:553)
                                                                           at com.google.gson.stream.JsonReader.peek(JsonReader.java:429)
                                                                           at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:202)
                                                                           at com.google.gson.TypeAdapter.fromJson(TypeAdapter.java:260)
                                                                           at retrofit2.GsonResponseBodyConverter.convert(GsonResponseBodyConverter.java:33)
                                                                           at retrofit2.GsonResponseBodyConverter.convert(GsonResponseBodyConverter.java:23)
                                                                           at retrofit2.OkHttpCall.parseResponse(OkHttpCall.java:154)
                                                                           at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:92)
                                                                           at okhttp3.RealCall$AsyncCall.execute(RealCall.java:133)

I know that It's possible to solve this issue Using Call<Void> but is there any other way to enforce OkHttp or Gson To accept Empty body?

Response Log:

 D/OkHttp: Date: Mon, 01 Feb 2016 08:32:10 GMT
D/OkHttp: Server: Apache/2.4.7 (Ubuntu)
D/OkHttp: X-Powered-By: PHP/5.5.9-1ubuntu4.13
D/OkHttp: Expires: Thu, 19 Nov 1981 08:52:00 GMT
D/OkHttp: Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
D/OkHttp: Pragma: no-cache
D/OkHttp: Access-Control-Allow-Origin: https://example.com
D/OkHttp: Access-Control-Allow-Methods: GET,POST,OPTIONS
D/OkHttp: Access-Control-Allow-Headers: Accept,Cache-Control,Pragma,Origin,Authorization,Content-Type,X-Requested-With,Cookie,*
D/OkHttp: Access-Control-Allow-Credentials: true
D/OkHttp: Content-Length: 0
D/OkHttp: Keep-Alive: timeout=5, max=99
D/OkHttp: Connection: Keep-Alive
D/OkHttp: Content-Type: application/json
D/OkHttp: OkHttp-Sent-Millis: 1454315528548
D/OkHttp: OkHttp-Received-Millis: 1454315528725
D/OkHttp: <-- END HTTP (0-byte body)
@FabianTerhorst
Copy link

Are you getting the same error when you don´t set a converter factory?

@roybrener
Copy link
Author

I didn't test without the converter. I guess it will not fail because GsonResponseBodyConverter.convert will not run. Is there any reason why i should check without the converter?

@JakeWharton
Copy link
Member

What behavior do you expect when the body is empty then?

You are telling Retrofit to deserialize the response as a certain object and Retrofit tells Gson to parse the stream into that type and the type is empty. This behavior already seems very reasonable to me.

@roybrener
Copy link
Author

Just return an "empty" pojo response (Like it was until now)... all fields null? It's a real problem...for example I didn't write the server side, just the Android App...The server side returns 200 with empty body...but there are times that the same call will return a 200 with a body message. I have nothing to do in situations like that

@JakeWharton
Copy link
Member

JakeWharton commented Feb 2, 2016

An empty pojo is {} in JSON.

You can write a delegating converter that does this, the behavior will not be added to Retrofit by default.

class NullOnEmptyConverterFactory implements Converter.Factory {
  @Override public Converter<ResponseBody, ?> responseBody(Type type, Annotation[] annotations, Retrofit retrofit) {
    final Converter<ResponseBody, ?> delegate = retrofit.nextResponseBodyConverter(this, type, annotations);
    return new Converter<>() {
      @Override public void convert(ResponseBody body) {
        if (body.contentLength() == 0) return null;
        return delegate.convert(body);
      }
    };
  }
}
Retrofit retrofit = new Retrofit.Builder()
    .endpoint(..)
    .addConverterFactory(new NullOnEmptyConverterFactory())
    .addConverterFactory(GsonConverterFactory.create())
    .build();

@TheHal85
Copy link

TheHal85 commented Apr 21, 2016

I had to implement this for a requirement from my services team. Here is the updated code I used for release version of Retrofit 2.0 that seems to be working in case anyone else comes across the issue.

public class NullOnEmptyConverterFactory extends Converter.Factory {

    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
        final Converter<ResponseBody, ?> delegate = retrofit.nextResponseBodyConverter(this, type, annotations);
            return new Converter<ResponseBody, Object>() {
                @Override
                public Object convert(ResponseBody body) throws IOException {
                    if (body.contentLength() == 0) return null;
                    return delegate.convert(body);                }
            };
    }
}

@nAkhmedov
Copy link

body.contentLength() always returns -1 on me. What to do?

@JakeWharton
Copy link
Member

Just read it. That means unknown content length.

On Wed, May 11, 2016, 11:51 PM nAkhmedov notifications@github.com wrote:

body.contentLength() always returns -1 on me. What to do?


You are receiving this because you modified the open/close state.

Reply to this email directly or view it on GitHub
#1554 (comment)

@nAkhmedov
Copy link

05-12 11:57:35.040 12039-13092/com.sms.sendsms D/OkHttp: <-- 200 OK http://www.yesplease.co.il/app/login.asp?username=zolim2&password=123456g (554ms)
05-12 11:57:35.041 12039-13092/com.sms.sendsms D/OkHttp: Cache-Control: must-revalidate,no-cache,private
05-12 11:57:35.041 12039-13092/com.sms.sendsms D/OkHttp: Content-Type: text/html; Charset=utf-8
05-12 11:57:35.041 12039-13092/com.sms.sendsms D/OkHttp: Expires: Wed, 11 May 2016 06:58:56 GMT
05-12 11:57:35.041 12039-13092/com.sms.sendsms D/OkHttp: Vary: Accept-Encoding
05-12 11:57:35.041 12039-13092/com.sms.sendsms D/OkHttp: Server: Microsoft-IIS/7.5
05-12 11:57:35.041 12039-13092/com.sms.sendsms D/OkHttp: X-Powered-By: Yes Please
05-12 11:57:35.041 12039-13092/com.sms.sendsms D/OkHttp: Server: Server
05-12 11:57:35.041 12039-13092/com.sms.sendsms D/OkHttp: Date: Thu, 12 May 2016 06:58:56 GMT
05-12 11:57:35.041 12039-13092/com.sms.sendsms D/OkHttp: Connection: Keep-Alive
05-12 11:57:35.041 12039-13092/com.sms.sendsms D/OkHttp: Set-Cookie: source=; path=/
05-12 11:57:35.041 12039-13092/com.sms.sendsms D/OkHttp: Set-Cookie: ASPSESSIONIDCSASTAQA=NCLNDGIBMFFIEGMOBHIABCEC; path=/
05-12 11:57:35.041 12039-13092/com.sms.sendsms D/OkHttp: OkHttp-Sent-Millis: 1463036254545
05-12 11:57:35.041 12039-13092/com.sms.sendsms D/OkHttp: OkHttp-Received-Millis: 1463036255040
05-12 11:57:35.041 12039-13092/com.sms.sendsms D/OkHttp: <-- END HTTP (0-byte body)
05-12 11:57:35.081 12039-12039/com.sms.sendsms W/System.err: java.io.EOFException: End of input at line 1 column 1

@nAkhmedov
Copy link

HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();//If need to logging, just uncomment
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(interceptor)
.build();
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(ContextConstants.APP_URL)
.addConverterFactory(new NullOnEmptyConverterFactory())
.addConverterFactory(GsonConverterFactory.create())
.client(client)
.build();

@AllenVork
Copy link

AllenVork commented Jul 19, 2016

I add the NullOnEmptyConverterFactory to the retrofit, and still gets the error.Then I find the order is important.

Retrofit retrofit = new Retrofit.Builder()
    .endpoint(..)
    .addConverterFactory(new NullOnEmptyConverterFactory()) //this should come first
    .addConverterFactory(GsonConverterFactory.create())
    .build();

@omarmiatello
Copy link

omarmiatello commented Jul 29, 2016

help for Kotlin devs

val nullOnEmptyConverterFactory = object : Converter.Factory() {
    fun converterFactory() = this
    override fun responseBodyConverter(type: Type, annotations: Array<out Annotation>, retrofit: Retrofit) = object : Converter<ResponseBody, Any?> {
        val nextResponseBodyConverter = retrofit.nextResponseBodyConverter<Any?>(converterFactory(), type, annotations)
        override fun convert(value: ResponseBody) = if (value.contentLength() != 0L) nextResponseBodyConverter.convert(value) else null
    }
}

@JakeWharton
Copy link
Member

@jacklt That is an inefficient solution. You have to look up the converter for every response. Look it up once in the factory so that it can be used over and over by the returned converter.

@omarmiatello
Copy link

@JakeWharton Why? I create only one Converter.Factory(). What's wrong with that code?

@JakeWharton
Copy link
Member

It calls nextResponseBodyConverter for every response.

@NightlyNexus
Copy link
Contributor

@jacklt You want to cache the result of nextResponseBodyConverter.
something like:

override fun responseBodyConverter(type: Type, annotations: Array<out Annotation>, retrofit: Retrofit): Converter<ResponseBody, *>? {
  val nextResponseBodyConverter = retrofit.nextResponseBodyConverter<Any>(this, type, annotations)
  return Converter<ResponseBody, Any> {
    if (it.contentLength() != 0L) {
      nextResponseBodyConverter.convert(it)
    } else {
      null
    }
  }
}

@omarmiatello
Copy link

omarmiatello commented Aug 12, 2016

Ok, thanks! That's the full version

val nullOnEmptyConverterFactory = object : Converter.Factory() {
    fun converterFactory() = this
    override fun responseBodyConverter(type: Type, annotations: Array<out Annotation>, retrofit: Retrofit) = object : Converter<ResponseBody, Any?> {
        val nextResponseBodyConverter = retrofit.nextResponseBodyConverter<Any?>(converterFactory(), type, annotations)
        override fun convert(value: ResponseBody) = if (value.contentLength() != 0L) nextResponseBodyConverter.convert(value) else null
    }
}

@Piasy
Copy link

Piasy commented Aug 21, 2016

In Gson's fromJson implementation, it take care of empty JSON string, by catching the EOFException and returning null, its comment said:

For compatibility with JSON 1.5 and earlier, we return null for empty documents instead of throwing.

So could it be possible that GsonResponseBodyConverter apply the same logic?

Any way, the NullOnEmptyConverterFactory is a good solution.

@JakeWharton
Copy link
Member

No, we won't be implementing that. An empty body is not valid JSON.

On Sun, Aug 21, 2016 at 12:58 AM Piasy notifications@github.com wrote:

In Gson's fromJson implementation
https://github.com/google/gson/blob/master/gson/src/main/java/com/google/gson/Gson.java#L891,
it take care of empty JSON string, by catching the EOFException and
returning null, its comment said:

For compatibility with JSON 1.5 and earlier, we return null for empty
documents instead of throwing.

So could it be possible that GsonResponseBodyConverter apply the same
logic?

Any way, the NullOnEmptyConverterFactory is a good solution.


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
#1554 (comment),
or mute the thread
https://github.com/notifications/unsubscribe-auth/AAEEEYMo8CG1Ae36ntGwnUxno5Gc4vISks5qh9sOgaJpZM4HQXza
.

@SaadBilal
Copy link

@JakeWharton will it deal with empty arrays like "array":[] ?

@JakeWharton
Copy link
Member

That's up to the serialization library you are using.

@Piasy
Copy link

Piasy commented Sep 4, 2016

I've got a good way to handle this, just want to share with you guys :)

I create an EmptyJsonLenientConverterFactory to deal with empty body:

public class EmptyJsonLenientConverterFactory extends Converter.Factory {

    private final GsonConverterFactory mGsonConverterFactory;

    public EmptyJsonLenientConverterFactory(GsonConverterFactory gsonConverterFactory) {
        mGsonConverterFactory = gsonConverterFactory;
    }

    @Override
    public Converter<?, RequestBody> requestBodyConverter(Type type,
            Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
        return mGsonConverterFactory.requestBodyConverter(type, parameterAnnotations,
                methodAnnotations, retrofit);
    }

    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
            Retrofit retrofit) {
        final Converter<ResponseBody, ?> delegateConverter =
                mGsonConverterFactory.responseBodyConverter(type, annotations, retrofit);
        return value -> {
            try {
                return delegateConverter.convert(value);
            } catch (EOFException e) {
                // just return null
                return null;
            }
        };
    }
}

And besides, my server return api error in HTTP 200 response, so I also create a YLApiErrorAwareConverterFactory to catch them:

public class YLApiErrorAwareConverterFactory extends Converter.Factory {

    private final EmptyJsonLenientConverterFactory mEmptyJsonLenientConverterFactory;

    public YLApiErrorAwareConverterFactory(
            EmptyJsonLenientConverterFactory emptyJsonLenientConverterFactory) {
        mEmptyJsonLenientConverterFactory = emptyJsonLenientConverterFactory;
    }

    @Override
    public Converter<?, RequestBody> requestBodyConverter(Type type,
            Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
        return mEmptyJsonLenientConverterFactory.requestBodyConverter(type, parameterAnnotations,
                methodAnnotations, retrofit);
    }

    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
            Retrofit retrofit) {
        final Converter<ResponseBody, ?> apiErrorConverter =
                mEmptyJsonLenientConverterFactory.responseBodyConverter(YLApiError.class,
                        annotations, retrofit);
        final Converter<ResponseBody, ?> delegateConverter =
                mEmptyJsonLenientConverterFactory.responseBodyConverter(type, annotations,
                        retrofit);
        return value -> {
            // read them all, then create a new ResponseBody for ApiError
            // because the response body is wrapped, we can't clone the ResponseBody correctly
            MediaType mediaType = value.contentType();
            String stringBody = value.string();
            try {
                Object apiError = apiErrorConverter
                        .convert(ResponseBody.create(mediaType, stringBody));
                if (apiError instanceof YLApiError && ((YLApiError) apiError).isApiError()) {
                    throw (YLApiError) apiError;
                }
            } catch (JsonSyntaxException notApiError) {
            }
            // then create a new ResponseBody for normal body
            return delegateConverter.convert(ResponseBody.create(mediaType, stringBody));
        };
    }
}

And to wire them up:

Retrofit retrofit = new Retrofit.Builder().client(okHttpClient)
        .baseUrl(URL)
        .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
        .addConverterFactory(new YLApiErrorAwareConverterFactory(
                new EmptyJsonLenientConverterFactory(GsonConverterFactory.create(gson))))
        .build();

@GJson
Copy link

GJson commented Oct 17, 2016

package com.dooioo.core.network;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;

import okhttp3.ResponseBody;
import retrofit2.Converter;
import retrofit2.Retrofit;

/**

  • Created by gjson on 2016/10/17.

  • Name NullOnEmptyConverterFactory

  • Version 1.0
    */
    public class NullOnEmptyConverterFactory extends Converter.Factory {

    @OverRide
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
    final Converter<ResponseBody, ?> delegate = retrofit.nextResponseBodyConverter(this, type, annotations);

    return new Converter<ResponseBody, Object>() {
    
        @Override
        public Object convert(ResponseBody value) throws IOException {
    
            if (value.contentLength() == 0) return null;
            return delegate.convert(value);
        }
    };
    

    }
    }

@Ztiany
Copy link

Ztiany commented Oct 17, 2016

  //ConverterFactory
  @Override
public Converter<ResponseBody, ?> responseBodyConverter(Type type,
                                                        Annotation[] annotations,
                                                        Retrofit retrofit) {
    final Converter<ResponseBody, ?> delegateConverter =        // 3
            mGsonConverterFactory.responseBodyConverter(type,
                    annotations, retrofit);

    return new Converter<ResponseBody, Object>() {
        @Override
        public Object convert(ResponseBody value) throws IOException {
            try {
                return delegateConverter.convert(value);            // 4
            } catch (JsonSyntaxException e) {
                // just return null
                Timber.e("Json covert error -->error : " + e);
                return ApiHelper.createErrorResult();     // return a errorResult
            }
        }
    };
}
      //crate errorResult
       public static HttpResult createErrorResult() {
             HttpResult httpResult = new HttpResult();
             httpResult.setCode(DATA_ERROR);
            return httpResult;
      }


      //hand error
      @Override
      public Observable<R> call(HttpResult<R> rHttpResult) {
          if (rHttpResult == null) {

               return Observable.error(new NetworkConnectionException());

             } else if (ApiHelper.isDataError(rHttpResult)) {

                 return Observable.error(new ServerErrorException());//server data error

             } else if (!ApiHelper.isSuccess(rHttpResult)) {

                 if (ApiHelper.isLoginExpired(rHttpResult)) {
                     RxBus.send(new EventLoginExpired());
                 }
                 return Observable.error(createException(rHttpResult));

             } else {
                 return Observable.just(rHttpResult.getData());
             }
         }

@grennis
Copy link

grennis commented Dec 13, 2016

The server I am talking to (which I do not control) does not return a valid content-length so solutions that rely on it do not work for me. Furthermore I am using kotlin and using moshi. This is the only solution I could get working. I realize this has a serious error that swallows EOFException but I do not see any other way of doing this. Any ideas?

class NullMoshiConverterFactory(val moshi: Moshi) : Converter.Factory() {
    val factory: MoshiConverterFactory

    init {
        factory = MoshiConverterFactory.create(moshi)
    }

    override fun requestBodyConverter(type: Type?, parameterAnnotations: Array<out Annotation>?, methodAnnotations: Array<out Annotation>?, retrofit: Retrofit?): Converter<*, RequestBody> {
        return factory.requestBodyConverter(type, parameterAnnotations, methodAnnotations, retrofit)
    }

    override fun responseBodyConverter(type: Type, annotations: Array<out Annotation>, retrofit: Retrofit): Converter<ResponseBody, *>? {
        return NullConverterFactory(factory.responseBodyConverter(type, annotations, retrofit))
    }

    class NullConverterFactory(val wrapped: Converter<ResponseBody, *>) : Converter<ResponseBody, Any> {
        override fun convert(value: ResponseBody?): Any? {
            try {
                return wrapped.convert(value)
            } catch (ex: EOFException) {
                return null
            }
        }
    }
}

@trinadhkoya
Copy link

trinadhkoya commented Apr 28, 2017

E/UncaughtException: java.lang.IllegalArgumentException: Expected receiver of type com.XXXX.models.SuccessResponse, but got retrofit2.ExecutorCallAdapterFactory$ExecutorCallbackCall

@xieqingle
Copy link

@nAkhmedov I met the same situation as you, how do you deal with it?

@nAkhmedov
Copy link

@SugerQ i tottaly forget what was the reason. Sorry

@paulpv
Copy link

paulpv commented Oct 25, 2017

Blast from the past, but here is a fully anonymous impl:

Retrofit retrofit = new Retrofit.Builder()
...
//
// Prevent empty body from throwing "java.io.EOFException: End of input at line 1 column 1 path $"
// Per: https://github.com/square/retrofit/issues/1554
//
.addConverterFactory(new Factory()
{
    @Nullable
    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit)
    {
        final Converter<ResponseBody, Object> delegate = retrofit.nextResponseBodyConverter(this, type, annotations);
        return new Converter<ResponseBody, Object>()
        {
            @Override
            public Object convert(@NonNull ResponseBody body)
                    throws IOException
            {
                return body.contentLength() != 0 ? delegate.convert(body) : null;
            }
        };
    }
})
...
.build();        

@kcorey
Copy link

kcorey commented Nov 30, 2017

Sadly, here's another 'me too'.

I've tried each and every one of these approaches, without success.

I'm creating my builder like this:

        Retrofit retrofit = new Retrofit.Builder()
                .baseUrl(url.toString())
                .client(client)
                .addConverterFactory(new NullOnEmptyConverterFactory())
                .addConverterFactory(converterFactory)
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .build();

This is in a static method, so I'm creating my NullConverter like this:

    public static class NullOnEmptyConverterFactory extends Converter.Factory {
        @Override
        public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
            final Converter<ResponseBody, ?> delegate = retrofit.nextResponseBodyConverter(this, type, annotations);
            Log.d(TAG, "responseBodyConverter: creating the NULL converter");
            return new Converter<ResponseBody, Object>() {
                @Override
                public Object convert(ResponseBody body) throws IOException {
                    Log.d(TAG, "convert: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~");
                    if (body.contentLength() == 0) {
                        Log.d(TAG, "convert: returning null");
                        return null;
                    } else {
                        Log.d(TAG, "convert: returning the delegate.convert result");
                        return delegate.convert(body);
                    }
                }
            };
        }
    }

Now, in every call where there's a body, I'm getting this in the log:

convert: ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
convert: returning the delegate.convert result

So, it would seem as if the Null Converter is at least trying to do its job.

However, on the call that is causing me difficulties (200 response, but empty body), I don't get those lines in the output.

All I can imagine is that the EOFException is being generated before the converter is getting a chance to protect us from the EOFException. So, even though I'm getting a 200 from the server, my code is in the error handler branch.

Yes, for pedants, I understand this is not "good", "valid" or "proper" HTML, and that the server needs to change. To that I would say that the world is not perfect. It shouldn't be so ridiculously hard to handle such a simple situation.

-Ken

@Shubhampatni86
Copy link

Shubhampatni86 commented Feb 13, 2018

In my case I'm receiving Response body "[text={"errorCode":"username_in_use","messages":null}]" but still while converting response it throwing EOFException. Any suggestion !!

converter.convert(responseBody)

@parcool
Copy link

parcool commented Aug 9, 2019

I've got a good way to handle this, just want to share with you guys :)

I create an EmptyJsonLenientConverterFactory to deal with empty body:

public class EmptyJsonLenientConverterFactory extends Converter.Factory {

    private final GsonConverterFactory mGsonConverterFactory;

    public EmptyJsonLenientConverterFactory(GsonConverterFactory gsonConverterFactory) {
        mGsonConverterFactory = gsonConverterFactory;
    }

    @Override
    public Converter<?, RequestBody> requestBodyConverter(Type type,
            Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
        return mGsonConverterFactory.requestBodyConverter(type, parameterAnnotations,
                methodAnnotations, retrofit);
    }

    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
            Retrofit retrofit) {
        final Converter<ResponseBody, ?> delegateConverter =
                mGsonConverterFactory.responseBodyConverter(type, annotations, retrofit);
        return value -> {
            try {
                return delegateConverter.convert(value);
            } catch (EOFException e) {
                // just return null
                return null;
            }
        };
    }
}

And besides, my server return api error in HTTP 200 response, so I also create a YLApiErrorAwareConverterFactory to catch them:

public class YLApiErrorAwareConverterFactory extends Converter.Factory {

    private final EmptyJsonLenientConverterFactory mEmptyJsonLenientConverterFactory;

    public YLApiErrorAwareConverterFactory(
            EmptyJsonLenientConverterFactory emptyJsonLenientConverterFactory) {
        mEmptyJsonLenientConverterFactory = emptyJsonLenientConverterFactory;
    }

    @Override
    public Converter<?, RequestBody> requestBodyConverter(Type type,
            Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit) {
        return mEmptyJsonLenientConverterFactory.requestBodyConverter(type, parameterAnnotations,
                methodAnnotations, retrofit);
    }

    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations,
            Retrofit retrofit) {
        final Converter<ResponseBody, ?> apiErrorConverter =
                mEmptyJsonLenientConverterFactory.responseBodyConverter(YLApiError.class,
                        annotations, retrofit);
        final Converter<ResponseBody, ?> delegateConverter =
                mEmptyJsonLenientConverterFactory.responseBodyConverter(type, annotations,
                        retrofit);
        return value -> {
            // read them all, then create a new ResponseBody for ApiError
            // because the response body is wrapped, we can't clone the ResponseBody correctly
            MediaType mediaType = value.contentType();
            String stringBody = value.string();
            try {
                Object apiError = apiErrorConverter
                        .convert(ResponseBody.create(mediaType, stringBody));
                if (apiError instanceof YLApiError && ((YLApiError) apiError).isApiError()) {
                    throw (YLApiError) apiError;
                }
            } catch (JsonSyntaxException notApiError) {
            }
            // then create a new ResponseBody for normal body
            return delegateConverter.convert(ResponseBody.create(mediaType, stringBody));
        };
    }
}

And to wire them up:

Retrofit retrofit = new Retrofit.Builder().client(okHttpClient)
        .baseUrl(URL)
        .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
        .addConverterFactory(new YLApiErrorAwareConverterFactory(
                new EmptyJsonLenientConverterFactory(GsonConverterFactory.create(gson))))
        .build();

what's your YLApiError?

@klaszlo8207
Copy link

Please reopen it, it is not working. When the server has no body and code is 4xx.

@BMarton
Copy link

BMarton commented Nov 5, 2020

A little help would be much appreciated here. Unfortunately I can't change the API I try to consume and one particular endpoint returns 202 - without any payload, no "{}", nothing - until the data is ready, then it'll return with a 200 and the proper payload.

When I to parse the 202 response it throws the exception. Any idea how to solve this? Is there a way to handle this - to handle the null response part and the valid payload too?

This is the converter:

internal val nullOnEmptyConverterFactory = object : Converter.Factory() {
    fun converterFactory() = this
    override fun responseBodyConverter(
        type: Type,
        annotations: Array<out Annotation>,
        retrofit: Retrofit
    ) = object : Converter<ResponseBody, Any?> {
        val nextResponseBodyConverter =
            retrofit.nextResponseBodyConverter<Any?>(converterFactory(), type, annotations)

        override fun convert(value: ResponseBody) =
            if (value.contentLength() != 0L) nextResponseBodyConverter.convert(value) else null
    }
}
fun provideRetrofit(factory: Gson, client: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .client(client)
            .addConverterFactory(nullOnEmptyConverterFactory)
            .addConverterFactory(ScalarsConverterFactory.create()) // used for jsonp
            .addConverterFactory(GsonConverterFactory.create(factory))
            .addCallAdapterFactory(CoroutineCallAdapterFactory())
            .baseUrl(ApiService.getBaseUrl())
            .build()
    }

This is the error message I get when I'm using HttpLoggingInterceptor to monitor the calls:

2020-11-05 10:52:42.294 1878-1878 error: java.io.EOFException
        at okio.RealBufferedSource.require(RealBufferedSource.kt:55)
        at okio.GzipSource.consumeHeader(GzipSource.kt:104)
        at okio.GzipSource.read(GzipSource.kt:62)
        at okio.RealBufferedSource.request(RealBufferedSource.kt:62)
        at okhttp3.logging.HttpLoggingInterceptor.intercept(HttpLoggingInterceptor.java:253)
        at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:112)
        at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:87)
        at okhttp3.RealCall.getResponseWithInterceptorChain(RealCall.kt:184)
        at okhttp3.RealCall$AsyncCall.run(RealCall.kt:136)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:919)

And this is the error message I get when I don't use the LogginInterceptor:

2020-11-05 10:59:35.246 2797-2797 error: java.io.EOFException
        at okio.RealBufferedSource.require(RealBufferedSource.kt:55)
        at okio.GzipSource.consumeHeader(GzipSource.kt:104)
        at okio.GzipSource.read(GzipSource.kt:62)
        at okio.RealBufferedSource.read(RealBufferedSource.kt:41)
        at okio.ForwardingSource.read(ForwardingSource.kt:29)
        at retrofit2.OkHttpCall$ExceptionCatchingResponseBody$1.read(OkHttpCall.java:288)
        at okio.RealBufferedSource.select(RealBufferedSource.kt:93)
        at okhttp3.internal.Util.readBomAsCharset(Util.kt:256)
        at okhttp3.ResponseBody$BomAwareReader.read(ResponseBody.kt:208)
        at com.google.gson.stream.JsonReader.fillBuffer(JsonReader.java:1295)
        at com.google.gson.stream.JsonReader.nextNonWhitespace(JsonReader.java:1333)
        at com.google.gson.stream.JsonReader.consumeNonExecutePrefix(JsonReader.java:1576)
        at com.google.gson.stream.JsonReader.doPeek(JsonReader.java:534)
        at com.google.gson.stream.JsonReader.peek(JsonReader.java:425)
        at com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:207)
        at retrofit2.converter.gson.GsonResponseBodyConverter.convert(GsonResponseBodyConverter.java:39)
        at retrofit2.converter.gson.GsonResponseBodyConverter.convert(GsonResponseBodyConverter.java:27)
        at data.network.util.NullOnEmptyConverterFactoryKt$nullOnEmptyConverterFactory$1$responseBodyConverter$1.convert(NullOnEmptyConverterFactory.kt:29)
        at data.network.util.NullOnEmptyConverterFactoryKt$nullOnEmptyConverterFactory$1$responseBodyConverter$1.convert(NullOnEmptyConverterFactory.kt:18)
        at retrofit2.OkHttpCall.parseResponse(OkHttpCall.java:225)
        at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:121)
        at okhttp3.RealCall$AsyncCall.run(RealCall.kt:138)
        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
        at java.lang.Thread.run(Thread.java:919)

@andrewdittmer
Copy link

andrewdittmer commented Nov 17, 2020

Not sure if this helps, but I was able to use Response<Unit> as mentioned in here to prevent crash. #3075

@BMarton
Copy link

BMarton commented Nov 23, 2020

Thanks @andrewdittmer! After a few hours of extensive digging around on SO and in github tickets I found out that my issue is caused by the backend. The API response lacks proper header data, and that's the reason I get the exception. The thread that helped me figure it out: square/okhttp#3646

@servatti
Copy link

servatti commented Mar 5, 2021

Just a heads up, I was facing this behaviour (null body exception) even with the solutions above. Then, I updated my API to return 200 instead of 204 (no content) and it worked.

Note: As I'm using RxJava, I needed to use Observable<Unit> as well.

@satyasivaprasad
Copy link

body.contentLength() always returns -1 on me. What to do?

I am also facing the same issue. means "Empty Body" and content length is -1, so after adding NullOnEmptyConverterFactory also getting the issue.

I think we should not add contentLength != -1 condition inside NullOnEmptyConverterFactory because square/okhttp#3309

Is there any solution?

@crisu83
Copy link

crisu83 commented Oct 14, 2021

Here is my solution in Kotlin (I found some of the above answers a bit confusing):

class NullOnEmptyConverterFactory @Inject constructor() : Converter.Factory() {

    override fun responseBodyConverter(
        type: Type,
        annotations: Array<out Annotation>,
        retrofit: Retrofit
    ) = Converter<ResponseBody, Any?> {
        if (it.contentLength() != 0L) retrofit.nextResponseBodyConverter<Any?>(
            this,
            type,
            annotations
        ).convert(it) else null
    }
}

@npwork
Copy link

npwork commented Dec 11, 2021

Here is my solution:

class NullOnEmptyConverterFactory extends Converter.Factory {
    @Override
    public Converter<ResponseBody, ?> responseBodyConverter(Type type, Annotation[] annotations, Retrofit retrofit) {
        final Converter<ResponseBody, ?> delegate = retrofit.nextResponseBodyConverter(this, type, annotations);
        return (Converter<ResponseBody, Object>) body -> {
            if (body.source().exhausted()) return null;
            return delegate.convert(body);
        };
    }
}

@eriknyk
Copy link

eriknyk commented May 6, 2023

@crisu83 @npwork did you test it?

I've tested in retrofit v2.9.0 and contentLegth() is always -1,
and it makes sense, since it will be set only when the response body is consumed by calling [ResponseBody instance].bytes() or [ResponseBody instance].byteString() or +other methods, but you trying to use a default value (-1 only) and it is set in the delegated converter i.e. GsonConverter. when body is consumed.

@yaugenka
Copy link

yaugenka commented Jun 2, 2023

Any reasons why the use of body.source().exhausted() is discouraged?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests