Giter Site home page Giter Site logo

eithernet's Introduction

EitherNet

A pluggable sealed API result type for modeling Retrofit responses.

Usage

By default, Retrofit uses exceptions to propagate any errors. This library leverages Kotlin sealed types to better model these responses with a type-safe single point of return and no exception handling needed!

The core type for this is ApiResult<out T, out E>, where T is the success type and E is a possible error type.

ApiResult has two sealed subtypes: Success and Failure. Success is typed to T with no error type and Failure is typed to E with no success type. Failure in turn is represented by four sealed subtypes of its own: Failure.NetworkFailure, Failure.ApiFailure, Failure.HttpFailure, and Failure.UnknownFailure. This allows for simple handling of results through a consistent, non-exceptional flow via sealed when branches.

when (val result = myApi.someEndpoint()) {
  is Success -> doSomethingWith(result.response)
  is Failure -> when (result) {
    is NetworkFailure -> showError(result.error)
    is HttpFailure -> showError(result.code)
    is ApiFailure -> showError(result.error)
    is UnknownFailure -> showError(result.error)
  }
}

Usually, user code for this could just simply show a generic error message for a Failure case, but the sealed subtypes also allow for more specific error messaging or pluggability of error types.

Simply change your endpoint return type to the typed ApiResult and include our call adapter and delegating converter factory.

interface TestApi {
  @GET("/")
  suspend fun getData(): ApiResult<SuccessResponse, ErrorResponse>
}

val api = Retrofit.Builder()
  .addConverterFactory(ApiResultConverterFactory)
  .addCallAdapterFactory(ApiResultCallAdapterFactory)
  .build()
  .create<TestApi>()

If you don't have custom error return types, simply use Unit for the error type.

Decoding Error Bodies

If you want to decode error types in HttpFailures, annotate your endpoint with @DecodeErrorBody:

interface TestApi {
  @DecodeErrorBody
  @GET("/")
  suspend fun getData(): ApiResult<SuccessResponse, ErrorResponse>
}

Now a 4xx or 5xx response will try to decode its error body (if any) as ErrorResponse. If you want to contextually decode the error body based on the status code, you can retrieve a @StatusCode annotation from annotations in a custom Retrofit Converter.

// In your own converter factory.
override fun responseBodyConverter(
  type: Type,
  annotations: Array<out Annotation>,
  retrofit: Retrofit
): Converter<ResponseBody, *>? {
  val (statusCode, nextAnnotations) = annotations.statusCode()
    ?: return null
  val errorType = when (statusCode.value) {
    401 -> Unauthorized::class.java
    404 -> NotFound::class.java
    // ...
  }
  val errorDelegate = retrofit.nextResponseBodyConverter<Any>(this, errorType.toType(), nextAnnotations)
  return MyCustomBodyConverter(errorDelegate)
}

Note that error bodies with a content length of 0 will be skipped.

Plugability

A common pattern for some APIs is to return a polymorphic 200 response where the data needs to be dynamically parsed. Consider this example:

{
  "ok": true,
  "data": {
    ...
  }
}

The same API may return this structure in an error event

{
  "ok": false,
  "error_message": "Please try again."
}

This is hard to model with a single concrete type, but easy to handle with ApiResult. Simply throw an ApiException with the decoded error type in a custom Retrofit Converter and it will be automatically surfaced as a Failure.ApiFailure type with that error instance.

@GET("/")
suspend fun getData(): ApiResult<SuccessResponse, ErrorResponse>

// In your own converter factory.
class ErrorConverterFactory : Converter.Factory() {
  override fun responseBodyConverter(
    type: Type,
    annotations: Array<out Annotation>,
    retrofit: Retrofit
  ): Converter<ResponseBody, *>? {
    // This returns a `@ResultType` instance that can be used to get the error type via toType()
    val (errorType, nextAnnotations) = annotations.errorType() ?: return null
    return ResponseBodyConverter(errorType.toType())
  }

  class ResponseBodyConverter(
    private val errorType: Type
  ) : Converter<ResponseBody, *> {
    override fun convert(value: ResponseBody): String {
      if (value.isErrorType()) {
        val errorResponse = ...
        throw ApiException(errorResponse)
      } else {
        return SuccessResponse(...)
      }
    }
  }
}

Retries

A common pattern in making network requests is to retry with exponential backoff. EitherNet ships with a highly configurable retryWithExponentialBackoff() function for this case.

// Defaults for reference
val result = retryWithExponentialBackoff(
  maxAttempts = 3,
  initialDelay = 500.milliseconds,
  delayFactor = 2.0,
  maxDelay = 10.seconds,
  jitterFactor = 0.25,
  onFailure = null, // Optional Failure callback for logging
) {
    api.getData()
}

Testing

EitherNet ships with a Test Fixtures artifact containing a EitherNetController API to allow for easy testing with EitherNet APIs. This is similar to OkHttp’s MockWebServer, where results can be enqueued for specific endpoints.

Simply create a new controller instance in your test using one of the newEitherNetController() functions.

val controller = newEitherNetController<PandaApi>() // reified type

Then you can access the underlying faked api property from it and pass that on to whatever’s being tested.

// Take the api instance from the controller and pass it to whatever's being tested
val provider = PandaDataProvider(controller.api)

Finally, enqueue results for endpoints as needed.

// Later in a test you can enqueue results for specific endpoints
controller.enqueue(PandaApi::getPandas, ApiResult.success("Po"))

You can also optionally pass in full suspend functions if you need dynamic behavior

controller.enqueue(PandaApi::getPandas) {
  // This is a suspend function!
  delay(1000)
  ApiResult.success("Po")
}

In instrumentation tests with DI, you can provide the controller and its underlying API in a test module and replace the standard one. This works particularly well with Anvil.

@ContributesTo(
  scope = UserScope::class,
  replaces = [PandaApiModule::class] // Replace the standard module
)
@Module
object TestPandaApiModule {
  @Provides
  fun providePandaApiController(): EitherNetController<PandaApi> = newEitherNetController()

  @Provides
  fun providePandaApi(
    controller: EitherNetController<PandaApi>
  ): PandaApi = controller.api
}

Then you can inject the controller in your test while users of PandaApi will get your test instance.

Java Interop

For Java interop, there is a limited API available at JavaEitherNetControllers.enqueueFromJava.

Validation

EitherNetController will run some small validation on API endpoints under the hood. If you want to add your own validations on top of this, you can provide implementations of ApiValidator via ServiceLoader. See ApiValidator's docs for more information.

Installation

Maven Central

dependencies {
  implementation("com.slack.eithernet:eithernet:<version>")

  // Test fixtures
  testImplementation(testFixtures("com.slack.eithernet:eithernet:<version>"))
}

Snapshots of the development version are available in Sonatype's snapshots repository.

License

Copyright 2020 Slack Technologies, LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

eithernet's People

Contributors

danieldisu avatar flavioarfaria avatar goooler avatar jdsm01 avatar jvmname avatar okamayana-tinyspeck avatar rciovati avatar roach avatar zacsweers avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

eithernet's Issues

[FEATURE] Support declarative error model binding

Description

At @tonalfitness, we use microservices. Many endpoints across different services use HTTP error codes with meaningful bodies. Turns out the body for a given status code might be totally different for two endpoints. Because of that, the advice provided here doesn't really work for us because the status code itself is not enough to know what type to return.

Here's what would be great:

interface MyApi {
    @DecodeErrorBody
    @POST("/some/path")
    suspend fun someEndpoint(): ApiResult<SuccessModel, GenericErrorModel>
}
sealed class GenericErrorModel {
    @HttpError(400, 403)
    @JsonClass(generateAdapter = true)
    data class SpecificErrorModel1(
        @Json(name = "intProperty") val intProperty: Int,
    ) : GenericErrorModel()

    @HttpError(500)
    @JsonClass(generateAdapter = true)
    data class SpecificErrorModel2(
        @Json(name = "stringArrayProperty") val stringProperty: String,
        @Json(name = "booleanProperty") val booleanProperty: Boolean,
    ) : GenericErrorModel()
}

The approach above is possible by extracting the response type inside a custom converter factory and then checking for a potential sealed error type. The actual response status code could be matched against annotated subtypes to figure out what model to deserialize the response to.

Correctness—like forgetting to annotate a subtype, or having more than one subtype bound to the same status code—could be enforced at compile time if either code generation or reflection is used (with a lint rule in this case).

Not sure if this is a too specific use case of if it's something that also makes sense to a broader audience that uses EitherNet.

Requirements (place an x in each of the [ ])

  • I've read and understood the Contributing guidelines and have done my best effort to follow them.
  • I've read and agree to the Code of Conduct.
  • I've searched for any related issues and avoided creating a duplicate issue.

Api<Unit, Throwable> crashes with NPE when http code is 204 or 205

Describe the bug

When using Api<Unit, Throwable> and the response is a http 204 or 2005 the app crashes with a NPE with the following stacktrace

at retrofit2.KotlinExtensions$await$2$2.onResponse(KotlinExtensions.kt:43)
at com.slack.eithernet.ApiResultCallAdapterFactory$ApiResultCallAdapter$adapt$1$enqueue$1.onResponse(ApiResult.kt:313)
at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:161)
at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)

This is due to retrofit passing a null body on OkHttpCall.java #parseResponse when http code is 204 or 205. ApiResult will then call callback.onResponse(call, Response.success(withTag)) with null withTag, which will then crash on retrofit's KotlinExtensions

Requirements (place an x in each of the [ ])**

  • I've read and understood the Contributing guidelines and have done my best effort to follow them.
  • I've read and agree to the Code of Conduct.
  • I've searched for any related issues and avoided creating a duplicate issue.

To Reproduce

Steps to reproduce the behavior:
Have a retrofit method that returns ApiResult<Unit, Throwable> and the response returns a http code 204 or 205.

Expected behavior

App behaves normally as it does with http error 200s

Screenshots

If applicable, add screenshots to help explain your problem.

Reproducible in: 1.4.0

Project version:

OS version(s): 12

Additional context

KotlinNullPointerException when api call returns 204 No Content with null response

Describe the bug

A clear and concise description of what the bug is.

When I make an API call that returns a 204 No Content and the response in null I get

kotlin.KotlinNullPointerException: Response from com.mediciland.datacollector.android.data.network.block.BlockApis.getBlock was null but response body type was declared as non-null
        at retrofit2.KotlinExtensions$await$2$2.onResponse(KotlinExtensions.kt:43)
        at com.slack.eithernet.ApiResultCallAdapterFactory$ApiResultCallAdapter$adapt$1$enqueue$1.onResponse(ApiResult.kt:245)
        at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:161)
        at com.google.firebase.perf.network.InstrumentOkHttpEnqueueCallback.onResponse(InstrumentOkHttpEnqueueCallback.java:69)
        at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
        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:764)

Requirements (place an x in each of the [ ])

  • I've read and understood the Contributing guidelines and have done my best effort to follow them.
  • I've read and agree to the Code of Conduct.
  • I've searched for any related issues and avoided creating a duplicate issue.

To Reproduce

Steps to reproduce the behavior:
The retrofit call is defined as

  @DecodeErrorBody
  @GET("block/{blockId}")
  suspend fun get(@Path("blockId") blockId: Long): ApiResult<BlockDto?, ErrorDto>

Expected behavior

As long as the the ApiResult response type is defined as nullable I would expect to not get a NPE

[BUG] test fixtures do not seem to be published

Describe the bug

I tried to add the test fixtures to my gradle project:

testImplementation testFixtures("com.slack.eithernet:eithernet:1.2.0")

But my test sources don't seem to pick up the test-fixture dependencies.

When I run dependencyInsight i get:

:dependencyInsight
com.slack.eithernet:eithernet:1.2.0 FAILED
   Failures:
      - Could not resolve com.slack.eithernet:eithernet:1.2.0.
          - Unable to find a variant of com.slack.eithernet:eithernet:1.2.0 providing the requested capability com.slack.eithernet:eithernet-test-fixtures:
               - Variant apiElements provides com.slack.eithernet:eithernet:1.2.0
               - Variant runtimeElements provides com.slack.eithernet:eithernet:1.2.0
               - Variant testFixturesApiElements provides com.slack.eithernet:EitherNet-test-fixtures:1.2.0
               - Variant testFixturesRuntimeElements provides com.slack.eithernet:EitherNet-test-fixtures:1.2.0


Which, according to gradle docs seem to be cause by missing gradle module metadata.

I'm no gradle expert, so perhaps I'm doing something wrong.

Requirements (place an x in each of the [ ])**

  • I've read and understood the Contributing guidelines and have done my best effort to follow them.
  • I've read and agree to the Code of Conduct.
  • I've searched for any related issues and avoided creating a duplicate issue.

Reproducible in:

{project_name} version: 1.2.0

Support interop with other call adapters

My sense was that this should Just Work™️ with other adapters, but upon further inspection we seem to be running up against a Java reflection limitation with regard to nested generic return types.

public Single<ApiResult<String, String>> foo();

Retrofit only passes on an ApiResult class and not the full ApiResult<String, String> type. Upon further digging, this seems to be coming from Java itself - the Method.getGenericReturnType() returns a Single<ApiResult>.

Not sure we can really work around this, will dig around a bit. I suspect a proper fix, if any, would require fixing in Retrofit itself.

(how) should we handle error bodies in 4xx responses?

Does this fit in ApiFailure or HttpFailure somewhere?

Some ideas:

A. Reuse ApiFailure

In this event, upon 4xx responses we would attempt to request a delegate converter to decode the error body and report in an ApiFailure with a code.

Awkward because it combines ApiFailure and HttpFailure

B. Add an error to HttpFailure

Simple, but potentially awkward because we then add a type to HttpFailure too. If we do this, I think we should reuse the same error type as ApiError

Other questions

  • How do we enable this?
    • parameter to the call adapter factory?
    • @DecodeErrorBody opt-in annotation?
  • What if there are multiple error types? Some APIs return different errors for different status codes
    • Could we pass the error code via annotation in the converter lookup, similar to how we use @ResultType

ClassCastException in ApiResultConverterFactory when minify is enabled

Describe the bug

Exception stack:

Caused by: java.lang.ClassCastException: java.lang.Class cannot be cast to java.lang.reflect.ParameterizedType
	at com.slack.eithernet.ApiResultConverterFactory.responseBodyConverter(ApiResult.kt:3)
	at retrofit2.Retrofit.nextResponseBodyConverter(Retrofit.java:6)
	at retrofit2.HttpServiceMethod.parseAnnotations(HttpServiceMethod.java:383)
	... 31 more

Only happens when minifyEnabled is true with Android Gradle Plugin version 7.0.

Requirements (place an x in each of the [ ])**

  • I've read and understood the Contributing guidelines and have done my best effort to follow them.
  • I've read and agree to the Code of Conduct.
  • I've searched for any related issues and avoided creating a duplicate issue.

To Reproduce

Steps to reproduce the behavior:

Use build with minfiyEnabled set to true. Observe exception when creating the Retrofit API instance.

Expected behavior

Exception should not happen regardless of minifyEnabled configuration.

Additional context

Add any other context about the problem here.

Retrofit converter for wrapped responses

Hi, can provide some examples how to handle wrapping responses please.

I need help to parse this json response:

{
"statusCode:" 0,
"statusMessage": "OK",
"resultset": {}
}

data class GenericErrorResponse(
    val statusCode: Int,
    val statusMessage: String
)
data class ApiWrappedResponse<T>(
    val statusCode: Int,
    val statusMessage: String,
    val resultset: T
)
@GET("/")
suspend fun fetchActionRules(): ApiResult<List<ActionRule>, GenericErrorResponse>
object DeEnvelopingConverter : Converter.Factory() {
    override fun responseBodyConverter(
        type: Type,
        annotations: Array<out Annotation>,
        retrofit: Retrofit
    ): Converter<ResponseBody, *>? {
       // This returns a `@ResultType` instance that can be used to get the error type via toType()
        val (errorType, nextAnnotations) = annotations.errorType() ?: return null
        val delegateConverter = retrofit.nextResponseBodyConverter<ApiWrappedResponse<Any>>(this, errorType.toType(), nextAnnotations)
        return ResponseBodyConverter(delegateConverter)
    }
}

private class ResponseBodyConverter<T>(private val delegate: Converter<ResponseBody, ApiWrappedResponse<T>>) : Converter<ResponseBody, T> {
    override fun convert(responseBody: ResponseBody): T? {
        return delegate.convert(responseBody)?.let {
            if (it.statusCode == 0) it.resultset else throw ApiException(GenericErrorResponse(it.statusCode, it.statusMessage))
        }
    }
}

========
This return ApiResult.Failure.UnknownFailure
cause -> java.lang.ClassCastException: GenericErrorResponse cannot be cast to ApiWrappedResponse

I found some information in Retrofit
square/retrofit#2179

https://medium.com/hackernoon/retrofit-converter-for-wrapped-responses-8919298a549c

Unable to create converter for class com.slack.eithernet.ApiResult when using Nothing with Moshi

@WorkerThread
    suspend fun fetchPokemonList(
        page: Int,
        onSuccess: () -> Unit,
        onError: (String) -> Unit
    ) = flow {
        var pokemons = pokemonDao.getPokemonList(page)
        if (pokemons.isEmpty()) {
            when(val result = apiClient.fetchPokemonList(page = page)) {
                is ApiResult.Success -> {
                    pokemons = result.response.results
                    pokemons.forEach { pokemon -> pokemon.page = page }
                    pokemonDao.insertPokemonList(pokemons)
                    emit(pokemons)
                    onSuccess()
                }
                is ApiResult.Failure -> onError("Error!")

            }
        } else {
            emit(pokemons)
            onSuccess()
        }
    }.flowOn(Dispatchers.IO)`

ApiClient.kt

suspend fun fetchPokemonList(
        page: Int
    ) = apiService.fetchPokemonList(
        limit = PAGING_SIZE,
        offset = page * PAGING_SIZE
    )

ApiService.kt

@GET("pokemon")
    suspend fun fetchPokemonList(
        @Query("limit") limit: Int = 20,
        @Query("offset") offset: Int = 0
    ): ApiResult<PokemonResponse, Nothing>
@Provides
    @Singleton
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .client(okHttpClient)
            .baseUrl(BuildConfig.BASE_URL)
            .addConverterFactory(MoshiConverterFactory.create())
            .addConverterFactory(ApiResultConverterFactory)
            .addCallAdapterFactory(ApiResultCallAdapterFactory)
            .build()
    }

    @Provides
    @Singleton
    fun provideApiService(retrofit: Retrofit): ApiService {
        return retrofit.create(ApiService::class.java)
    }

    @Provides
    @Singleton
    fun provideApiClient(apiService: ApiService): ApiClient {
        return ApiClient(apiService)
    }`

Am I missing something?

[BUG]

Describe the bug

A clear and concise description of what the bug is.

Requirements (place an x in each of the [ ])**

  • I've read and understood the Contributing guidelines and have done my best effort to follow them.
  • I've read and agree to the Code of Conduct.
  • I've searched for any related issues and avoided creating a duplicate issue.

To Reproduce

Steps to reproduce the behavior:

Expected behavior

A clear and concise description of what you expected to happen.

Screenshots

If applicable, add screenshots to help explain your problem.

Reproducible in:

{project_name} version:

{platform_name} version:

OS version(s):

Additional context

Add any other context about the problem here.

[BUG] Exception when looking for an annotation that doesn't exist

Describe the bug

When the target annotation is missing, we get an exception trying to access an array position that is out of bounds at line 84.

private fun <A : Any> Array<out Annotation>.nextAnnotations(type: Class<A>): Pair<A, Array<Annotation>>? {
var nextIndex = 0
val theseAnnotations = this
var resultType: A? = null
val nextAnnotations = arrayOfNulls<Annotation>(size - 1)
for (i in indices) {
val next = theseAnnotations[i]
if (type.isInstance(next)) {
@Suppress("UNCHECKED_CAST")
resultType = next as A
} else {
nextAnnotations[nextIndex] = next
nextIndex++
}
}
return if (resultType != null) {
@Suppress("UNCHECKED_CAST")
resultType to (nextAnnotations as Array<Annotation>)
} else {
null
}
}

The reason is because nextAnnotations is smaller than the original array by 1 and the loop code is optimistic about finding the target annotation in the original array. When this doesn't happen, the executed code path merely tries to copy the original array into nextAnnotations, overflowing it at the end.

Requirements (place an x in each of the [ ])**

  • I've read and understood the Contributing guidelines and have done my best effort to follow them.
  • I've read and agree to the Code of Conduct.
  • I've searched for any related issues and avoided creating a duplicate issue.

To Reproduce

Steps to reproduce the behavior:

  1. Write your own Converter.Factory and plug it to Retrofit after ApiResultConverterFactory;
  2. Call annotations.statusCode() inside responseBodyConverter();
  3. Get the exception thrown.

Expected behavior

Instead of getting an exception, we should either get:

  • null to (theseAnnotations as Array<Annotation>) which means we couldn't single out the target annotation (preferable IMO);
  • Simply null.

Reproducible in:

Project version: 1.2.1

OS version(s): Any

Additional context

If this is recognized as a bug, I'm happy to push a PR for review.

[BUG] v1.3.0 published with invalid Gradle Module Metadata

Describe the bug

The GMM for v1.3.0 contains invalid capabilities entries for the testFixtures variants so Gradle fails to resolve the dependency even if you are not relying on the test fixtures.

Capabilities in v1.2.1

Details
// curl -sL https://repo.maven.apache.org/maven2/com/slack/eithernet/eithernet/1.2.1/eithernet-1.2.1.module | jq '.variants[].capabilities? | select( . != null )'
[
  {
    "group": "com.slack.eithernet",
    "name": "eithernet-test-fixtures",
    "version": "1.2.1"
  }
]
[
  {
    "group": "com.slack.eithernet",
    "name": "eithernet-test-fixtures",
    "version": "1.2.1"
  }
]

Capabilities in v1.3.0

Details
// curl -sL https://repo.maven.apache.org/maven2/com/slack/eithernet/eithernet/1.3.0/eithernet-1.3.0.module | jq '.variants[].capabilities? | select( . != null )'
[
  {
    "group": "",
    "name": "eithernet-test-fixtures",
    "version": "unspecified"
  }
]
[
  {
    "group": "",
    "name": "eithernet-test-fixtures",
    "version": "unspecified"
  }
]
[
  {
    "group": "",
    "name": "eithernet-test-fixtures",
    "version": "unspecified"
  }
]

Build failure log

Details
* What went wrong:
Execution failed for task ':api:compileDebugKotlin'.
> Error while evaluating property 'filteredArgumentsMap' of task ':api:compileDebugKotlin'.
   > Could not resolve all files for configuration ':api:debugCompileClasspath'.
      > Could not resolve com.slack.eithernet:eithernet:1.3.0.
        Required by:
            project :api
         > Could not resolve com.slack.eithernet:eithernet:1.3.0.
            > Could not parse module metadata https://repo.maven.apache.org/maven2/com/slack/eithernet/eithernet/1.3.0/eithernet-1.3.0.module
               > missing 'group' at /variants[2]/capabilities[0]

Requirements

  • I've read and understood the Contributing guidelines and have done my best effort to follow them.
  • I've read and agree to the Code of Conduct.
  • I've searched for any related issues and avoided creating a duplicate issue.

To Reproduce

Add the EitherNet 1.3.0 dependency to a project and attempt to build it

Expected behavior

Gradle resolves the dependency and builds the project

Reproducible in:

Project version: 1.3.0

Additional context

I noticed this in a regular Renovate PR to my app so you can repro it there but it has a rather massive dependency tree so it's probably easier to use a fresh skeleton project .

[BUG] EitherNet + MultiDex + minSDK 19

Describe the bug

StackOverflowError during assembleDebug with minSDK 19 and multiDexEnabled true

AGPBI: {"kind":"error","text":"java.lang.StackOverflowError","sources":[{}],"tool":"D8"}
java.lang.StackOverflowError

No issue when setting min SDK to 21.

Requirements (place an x in each of the [ ])**

  • I've read and understood the Contributing guidelines and have done my best effort to follow them.
  • I've read and agree to the Code of Conduct.
  • I've searched for any related issues and avoided creating a duplicate issue.

To Reproduce

Steps to reproduce the behavior:

  • Set minSDK to 19
  • Add EitherNet library
  • Enable multidex
  • run ./gradlew app:assembleDebug

Expected behavior

Compilation works as expected.

Reproducible in:

https://gist.github.com/renaudmathieu/b1c7deb835b3a4590b30cc5183bbf47d

OS version(s):

Additional context

  • No issue when setting min SDK to 21.
  • Android Studio 4.1
  • gradle-6.5-all

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.