본문 바로가기
Android/Trouble Shoot

java.lang.IllegalStateException: Expected BEGIN_OBJECT but was STRING at line 1 column 1 path $

by jaesungLeee 2022. 3. 14.

최근 프로젝트를 진행하면서 네트워크 통신을 위해 Retrofit2 라이브러리와 비동기 처리를 지원하는 Coroutine을 함께 사용하는 경우가 많아지고 있다. 통신 후 받아오는 response의 status가 오직 200인 경우에는 크게 상관없지만 현재 진행 중인 프로젝트에서는 200, 401, 409 등 다양한 status의 response가 상황에 따라 넘어오게 된다. 

 

이번 포스팅은 다양한 HTTP Status 처리를 위해 리팩토링 도중 마주친 에러에 대한 포스팅이다.

 

 

1. 기존 코드

다양한 status 처리를 위해 ResultWrapper라는 sealed class를 사용하여 통신이 성공적 (Success)인지, 네트워크 통신 에러 (NetworkError)인지, 일반적인 status 에러 (GenericError)인지를 우선 확인하는 과정을 거치게 된다.

ResultWrapper에 대한 자세한 설명은 아래 포스팅을 참고하도록 하자. (작성중 ..)

 

아래 코드는 위 과정을 선 수행하기 위해 호출되는 함수이다.

suspend fun <T> safeApiCall(
    dispatcher: CoroutineDispatcher,
    apiCall: suspend () -> T): ResultWrapper<T> {
    return withContext(dispatcher) {
        try {
            ResultWrapper.Success(apiCall.invoke())
        } catch (throwable: Throwable) {
            when(throwable) {
                is IOException -> ResultWrapper.NetworkError
                is HttpException -> {
                    val code = throwable.code()
                    val errorBody = throwable.response()?.errorBody()?.toString()
                    Timber.d(code.toString())
                    Timber.d(errorBody)
                    val gsonErrorBody = Gson().fromJson(
                        errorBody,
                        ErrorBody::class.java
                    )
                    val message = gsonErrorBody.message
                    ResultWrapper.GenericError(code, message)
                }
                else -> ResultWrapper.GenericError(null, null)
            }
        }
    }
}

safeApiCall 함수는 CoroutineDispatcher 상에서 네트워크 작업을 하는 task의 결과를 try-catch로 확인하게 된다. 성공적인 결과라면 ResultWrapper.Success로 리턴하게 되지만 그렇지 않은 경우 catch문에서 확인하게 된다.

마주한 에러는 HttpException에서 errorBody로 변환하는 과정에서 발생한 에러이다.


2. 관련된 다양한 해결 방법들

해당 에러를 검색해보면 다양한 해결 방법들이 나온다. 많은 개발자 분들이 공통적으로 설명하는 것은 만들어 놓은 ErrorBody의 타입과 실제 받아온 responseBody의 타입이 맞지 않기 때문에 발생하는 에러라고 설명하고있다.

 

https://selfish-developer.com/entry/Expected-BEGINOBJECT-but-was-BEGINARRAY-at-line-1-column-15-path-documents

 

Expected BEGIN_OBJECT but was BEGIN_ARRAY at line 1 column 15 path $.documents

안드로이드 개발중 Gson과 Retrofit을 이용해 Json 데이터를 주고 받을 때 이런 에러를 보게 되는 경우가 있다 Caused by: com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGI..

selfish-developer.com

https://stackoverflow.com/questions/28418662/expected-begin-object-but-was-string-at-line-1-column-1?lq=1 

 

"Expected BEGIN_OBJECT but was STRING at line 1 column 1"

I have this method: public static Object parseStringToObject(String json) { String Object = json; Gson gson = new Gson(); Object objects = gson.fromJson(object, Object.class);

stackoverflow.com

 

HttpException을 통해 걸러지는 body들이 실제로 어떻게 들어오는지 확인하기 위해 로그를 확인해보던 중 문제점을 발견하게 되었다.

body를 눈여겨 봐야 한다.


3. toString() vs string()

ResponseBody의 toString()은 해당 인스턴스의 주소값으로 나오게 된다. 생각해보면 당연하다. ResponseBody는 클래스이기 때문이다.

 

내가 원하는 ResponseBody의 실제 body를 가져오기 위해서 제공하는 메소드는 바로 string()이다.

string() 메소드는 ResponseBody를 문자열로 반환하는 메소드이다. 다만 사용 시 주의점은 전체 response body를 메모리에 로드하게 되고 만일 response body가 매우 크게되면 OutOfMemory를 발생시킨다. 

 

string() 메소드는 일회성만 가능하며 두 번 호출하게 되면 NullPointerException을 발생시킨다. response가 메모리에 저장되어있는 상태가 아니기 때문이다.


4. 수정 코드 전문

suspend fun <T> safeApiCall(
    dispatcher: CoroutineDispatcher,
    apiCall: suspend () -> T): ResultWrapper<T> {
    return withContext(dispatcher) {
        try {
            ResultWrapper.Success(apiCall.invoke())
        } catch (throwable: Throwable) {
            when(throwable) {
                is IOException -> ResultWrapper.NetworkError
                is HttpException -> {
                    val code = throwable.code()
                    val errorBody = throwable.response()?.errorBody()?.string()
                    Timber.d(code.toString())
                    Timber.d(errorBody)
                    val gsonErrorBody = Gson().fromJson(
                        errorBody,
                        ErrorBody::class.java
                    )
                    val message = gsonErrorBody.message
                    ResultWrapper.GenericError(code, message)
                }
                else -> ResultWrapper.GenericError(null, null)
            }
        }
    }
}

 

아래 처럼 정상적으로 401 status 처리가 가능하다.


References

https://square.github.io/okhttp/4.x/okhttp/okhttp3/-response-body/

 

ResponseBody - OkHttp - OkHttp

//okhttp/okhttp3/ResponseBody ResponseBody [common]\ expect abstract class ResponseBody : Closeable A one-shot stream from the origin server to the client application with the raw bytes of the response body. Each response body is supported by an active con

square.github.io

https://jaejong.tistory.com/34

 

[안드로이드] Retrofit2 - ResponseBody 반환타입 출력

Retrofit - ResponseBody 원시 데이터 출력 Call - Retrofit으로 받아온 원시데이터를 가공없이 출력하기 Open API '공공데이터 포털' 측정소정보 조회 서비스 API - 'TM 기준좌표 조회' 응답결과를 JSON / XML 객..

jaejong.tistory.com