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

[Networking Stack] additionalErrorInterceptor(for:) doesn't work when subclassing LegacyInterceptorProvider #1408

Conversation

TizianoCoroneo
Copy link
Contributor

@TizianoCoroneo TizianoCoroneo commented Sep 25, 2020

The problem

The problem is: the additionalErrorInterceptor(for:) function is never called, if the InterceptorProvider class in use is a subclass of any InterceptorProvider that does not declare the additionalErrorInterceptor function.

For example, consider this CustomInterceptor type:

class CustomInterceptor: LegacyInterceptorProvider {
  // Note that we don't use `override` here.
  func additionalErrorInterceptor<Operation>(for operation: Operation) -> ApolloErrorInterceptor? where Operation : GraphQLOperation {
    ErrorInterceptor()
  }
}

Its superclass, LegacyInterceptorProvider, does not provide an implementation of additionalErrorInterceptor(for:), relying instead on the default implementation from the protocol, that returns nil.
Since its superclass does not implement this function, the implementation in CustomInterceptor is not overriding it, but declaring it anew. For some 🤯 reason, this means that the implementation that provides the additionalErrorInterceptor is never called. The default implementation in the extension of the InterceptorProvider protocol is called instead, returning nil.

The solution

I suggest 2 small changes to solve this issue, and remove this trap.

  • Remove the default implementation of additionalErrorInterceptor(for:) from extension InterceptorProvider. This will force base classes to implement it; the library expects the user to subclass one of the provided InterceptorProviders anyway, so I think it's acceptable.
  • Implement additionalErrorInterceptor(for:) in both LegacyInterceptorProvider and CodableInterceptorProvider, so that subclasses will have something to override, but keeping the nice nil default.

These changes make for a pretty smooth solution, with only one small breaking change: InterceptorProviders that did implement additionalErrorInterceptor(for:) before this change will have to add the override keyword, if they inherited from one of the provided InterceptorProviders.

Double checking

You can check out the first commit of this PR to see that the Unit test I added fails, reproducing the issue. Add the other commit, and everything is green again ✅

The problem is: the `additionalErrorInterceptor(for:)` function is never called, if the `InterceptorProvider` class in use is a subclass of any `InterceptorProvider` that *does not* declare the `additionalErrorInterceptor` function.

For example, consider this `CustomInterceptor` type:
```swift
class CustomInterceptor: LegacyInterceptorProvider {
  // Note that we don't use `override` here.
  func additionalErrorInterceptor<Operation>(for operation: Operation) -> ApolloErrorInterceptor? where Operation : GraphQLOperation {
    ErrorInterceptor()
  }
}
```

Its superclass, `LegacyInterceptorProvider`, does not provide an implementation of `additionalErrorInterceptor(for:)`, relying instead on the default implementation that returns `nil`.
Since its superclass does not implement this function, the implementation in `CustomInterceptor` is not overriding it, but declaring it anew. For some 🤯 reason, this means that the implementation that provides the `additionalErrorInterceptor` is never called. The default implementation in the extension of the `InterceptorProvider` protocol is called instead, returning `nil`.

I suggest 2 small changes to solve this issue, and remove this trap.
- Remove the default implementation of `additionalErrorInterceptor(for:)` from `extension InterceptorProvider`. This will force base classes to implement it; the library expects the user to subclass one of the provided InterceptorProviders anyway, so I think it's acceptable.
- Implement `additionalErrorInterceptor(for:)` in both `LegacyInterceptorProvider` and `CodableInterceptorProvider`, so that subclasses will have something to override, but keeping the nice `nil` default.

These changes make for a pretty smooth solution, with only one small breaking change: `InterceptorProvider`s that did implement `additionalErrorInterceptor(for:)` before this change will have to add the `override` keyword, if they inherited from one of the provided `InterceptorProvider`s.
Copy link
Contributor

@designatednerd designatednerd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi! Good point, though I think we can make this work without removing the default implementation.

Sources/Apollo/InterceptorProvider.swift Outdated Show resolved Hide resolved
Sources/Apollo/InterceptorProvider.swift Outdated Show resolved Hide resolved
Tests/ApolloTests/RequestChainTests.swift Show resolved Hide resolved
@designatednerd
Copy link
Contributor

(current test failure is an intermittent issue and unrelated, when you push up new commits I'll kick the CI server if needed)

@TizianoCoroneo
Copy link
Contributor Author

Hi! Good point, though I think we can make this work without removing the default implementation.

I thought that removing the default implementation from the protocol it would create a footgun for users of the library that create their own InterceptorProvider without subclassing one of the main ones:
If the base class does not implement the function, then other custom subclasses will incur in the same issue.

import Apollo

class BaseIP: InterceptorProvider {
    // no implementation for additionalErrorInterceptor
}

class SubIP: BaseIP {
    // implements the function, but incurs in the same issue.
}

It's kind of a corner case 🤷‍♂️ we'll do fine either way.

I spent a couple days rewriting a network layer with the new networking stack and it's 🔥🔥🔥 keep up the good work! 👏

@TizianoCoroneo
Copy link
Contributor Author

I've found why this specific issue happens: the subclass cannot override the default implementation of the protocol because... it needs to be implemented in the compiler 😅 it's one of the features mentioned in Swift's Generics Manifesto, under "Allowing subclasses to override requirements satisfied by defaults".

@designatednerd
Copy link
Contributor

If someone's creating their own InterceptorProvider I'd be really surprised if they're creating something with subclasses.

This is probably worth reminding people of in documentation rather than not including the default implementation, IMO.

@TizianoCoroneo
Copy link
Contributor Author

I'll put back the default implementation in the protocol extension, and remove the implementation from the various TestProviders in the tests.

I'll try to update the docs as well, but one warning: I'm a non-native speaker 😅

…n providers.

Added missing `override` keyword.
@TizianoCoroneo TizianoCoroneo force-pushed the tiziano/networking-stack-additional-error-handler-bug branch from aebea51 to d6087c6 Compare September 26, 2020 12:46
@designatednerd designatednerd merged commit dab6243 into apollographql:betas/networking-stack Sep 26, 2020
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

Successfully merging this pull request may close these issues.

2 participants