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

Not compatable with SWHttpTrafficRecorder #232

Closed
Amindv1 opened this issue Dec 19, 2016 · 14 comments
Closed

Not compatable with SWHttpTrafficRecorder #232

Amindv1 opened this issue Dec 19, 2016 · 14 comments

Comments

@Amindv1
Copy link

Amindv1 commented Dec 19, 2016

I wanted to point out that this pod isn't compatible with the SWHttpTrafficRecorder as it points out on the readme. Both this pod and that pod try to register their own versions of NSURLProtocol which means one of them is going to lose, so either you don't record the file or you don't stub the response. I spent a good chunk of time trying to figure out why they weren't working together and thought this should be noted on your readme, as it makes it look like it's been tested and it should work when in reality it doesn't.

@AliSoftware
Copy link
Owner

Thanks for the issue.

It has been tested… but separately, as you usually don't both stub and record what you stub, but rather record first and stub later.

But you're right in that it should be mentioned in the README that you should not use both at the same time but rather one after the other, so the README should be more clear about that.

@Amindv1
Copy link
Author

Amindv1 commented Dec 19, 2016

So the reason I have a two in one is that I try to automate everything. I attempt to record, if the file name for the request doesn't exist then I do record the request, otherwise if it does exist I stub the request based off of the file. This requires me to register both.

@AliSoftware
Copy link
Owner

I see.

I think you should still be able to register OHHTTPStubs manually (I don't remember of SWHttpTrafficRecorder offers an API to register the NSURLProtocol manually but OHHTTPStubs has setEnabled: and setEnabled: forSession: methods that should allow you to add OHHTTPStubs' internal NSURLProtocol manually even after SWHttpTrafficRecorder did set itself up and added is own, making them both intercept the requests (I indeed haven't tested this use case of supporting them both at the same time but it's worth a try as it should theoretically work if you do it this way)

@Amindv1
Copy link
Author

Amindv1 commented Dec 19, 2016

I thought that only one NSURLProtocol can be registered at a time? I don't think I understand your proposed solution. Can you show me what you mean? This would save me a lot of heartache, I was considering merging the two pods into one so I can merge their protocols and use them at the same time.

@AliSoftware
Copy link
Owner

No you can register as many as you want. Let me find a Mac in a minute so I can give you an example

@AliSoftware
Copy link
Owner

Ok, so, basically my proposed solution is for you to call OHHTTPStubs.sharedInstance.setEnabled(true, forSessionConfiguration: sessionConfig) (assuming you use a custom NSURLSession build from an NSURLSessionConfiguration, and not NSURLConnection nor the shared NSURLSession) before creating your NSURLSession with it.

You can find this method's implementation here. As you can see, this method doesn't replace the supported NSURLProtocols of an NSURLSession, but instead just inserts OHHTTPStubs's own NSURLProtocol at the top of the sessionConfig.protocolClasses array.

This insertion is automatically done on the two NSURLSessionConfiguration's defaultSessionConfiguration and ephemeralSessionConfiguration presets (thanks to some swizzling).

If you use SWHttpTrafficRecorder, it also inserts its own NSURLProtocol in this array (see here). But for this to work you'll have to:

  • Ensure you create your custom NSURLSessionConfiguration and set it up both for OHHTTPStubs AND for SWHttpTrafficRecorder before you create your NSURLSession(configuration: …) initializer, because when you create your NSURLSession from a configuration, the configuration is copied, so any further changes on the NSURLSessionConfiguration you used after creating the NSURLSession won't affect that NSURLSession retroactively (this is explained in Apple's doc on NSURLSession)
  • Ensure that both OHHTTPStubs an SWHttpTrafficRecorder did set up their NSURLProtocol in the right order and on the same NSURLSessionConfiguration

So, now that I've had time to re-read the implementation of both pods, I don't see why this wouldn't work:

  1. Ensure that you have the pod 'OHHTTPStubs' pod in your Podfile. If you only have pod 'OHHTTPStubs/Swift' for example, this subspec doesn't contain the NSURLSession-supporting subspec by default (this might change in the future, but for now the Swift subspec only contains the Swift helpers, not the other subspecs. See the dedicated section in the README about this
  2. Use this kind of code (I wrote it in Swift but should work the same in ObjC):
import Foundation

// Start with the default configuration
let config = URLSessionConfiguration.default

// If you indeed have the `OHHTTPStubs/NSURLSession` subspec listed in your `Podfile.lock`
// then OHHTTPStubs should have automatically  injected its own `NSURLProtocol` at this point
// You can check this using some NSLog:
NSLog("protocol classes after OHHTTPStubs: \(config.protocolClasses)") // this should contain `OHHTTPStubsProtocol` among others

do {
  // not sure about the translation of SWHttpTrafficRecorder API in Swift here, I'll let you adapt
  let started = try SWHttpTrafficRecorder.sharedRecorder().startRecording(path:nil, forSessionConfiguration:config)
  // At this point, SWHttpTrafficRecord should itself have added its own NSURLProtocol to the
  // protocolClasses array, intercepting the traffic first, BEFORE anybody else (including OHHTTPStubs)
  NSLog("Started: \(started)")
  NSLog("protocolClasses after SWHTR: \(config.protocolClasses)")
} catch {
  NSLog("Error while trying to start SWHttpTrafficRecorder")
}

// At this point, SWHttpTrafficRecorder is listed first in the protocolClasses array
// then OHHTTPStubs is next in line. This means that SWHTR will intercept requests first, then
// OHHTTPStubs will be given the ability to stub them next, then if the request passed thru both
// the request will finally hit the real world.

// If you want to make `OHHTTPStubs` intercept the requests BEFORE SWHttpTrafficRecorder
// Then you have to ask `OHHTTPStubs` to remove itself then re-insert itself at index 0
OHHTTPStubs.setEnabled(false, forSessionConfiguration: config) // remove first
OHHTTPStubs.setEnabled(true, forSessionConfiguration: config) // then reinsert, at index 0

// And ONLY then, create your NSURLSession, because if you create it before
// setting up the NSURLSessionConfiguration, changes in the configuration won't do anything
let session = URLSession(configuration: config)
let task = session.dataTask(with: yourURLHere)
task.resume()

@Amindv1
Copy link
Author

Amindv1 commented Dec 19, 2016

What's the difference between adding the protocol like so:

[NSURLProtocol registerClass:OHHTTPStubsProtocol.class];

and like so:

NSMutableArray * urlProtocolClasses = [NSMutableArray arrayWithArray:sessionConfig.protocolClasses]; Class protoCls = OHHTTPStubsProtocol.class; if (enable && ![urlProtocolClasses containsObject:protoCls]) { [urlProtocolClasses insertObject:protoCls atIndex:0]; } else if (!enable && [urlProtocolClasses containsObject:protoCls]) { [urlProtocolClasses removeObject:protoCls]; } sessionConfig.protocolClasses = urlProtocolClasses;?

If I register the class for NSURLProtocol does it register it for the default protocol, so any call to URLSessionConfiguration.default after adding a protocol will include the new protocol?

@AliSoftware
Copy link
Owner

The difference is that one applies to global sessions/connections, the other to sessions built with a given session configuration.

  • [NSURLProtocol registerClass:…] only applies to requests sent via NSURLConnection or [NSURLSession sharedSession], and does not apply to requests sent via an NSURLSession built from an NSURLSessionConfiguration
  • [NSURLSessionConfiguration setProtocolClasses:] is used by requests sent via an NSURLSession that has been created from that given NSURLSessionConfiguration, and doesn't affect the NSURLSession instances created by other NSURLSessionConfigurations nor requests sent via NSURLSession

For more info, see:

For in-depth understanding, you can also find detailed information in Apple's API documentation on NSURLSession / NSURLSessionConfiguration and the URL Loading System Programming Guide

@Amindv1
Copy link
Author

Amindv1 commented Dec 19, 2016

Alright maybe I'm confused here but something doesn't seem right. I was assuming that the setEnabled method is what was registering the class under the default session configuration, which is what AFNetworking was using. But after some testing I don't know if this is the case, how are you making it so that you stub AFNetworking requests? The reason I don't think it's the registerClass function is because SWHttpTrafficRecorder uses that to register its own protocol class but when I do:

print("before: ", URLSessionConfiguration.default.protocolClasses)
print("did register AFRecordingProtocol.self: ", URLProtocol.registerClass(AFRecordingProtocol.self))    // This is my custom recording protocol looks exactly like SW's    
print("after: ", URLSessionConfiguration.default.protocolClasses)

the before and after are the same:

before: Optional([OHHTTPStubsProtocol, _NSURLHTTPProtocol, _NSURLDataProtocol, _NSURLFTPProtocol, _NSURLFileProtocol, NSAboutURLProtocol])
did register AFRecordingProtocol.self: true
after: Optional([OHHTTPStubsProtocol, _NSURLHTTPProtocol, _NSURLDataProtocol, _NSURLFTPProtocol, _NSURLFileProtocol, NSAboutURLProtocol])

As you can see, OHHTTPStubsProtocol is being registered but my custom protocol isn't listed. How is OHHTTPStubs making it so that the class is being registered with AFNetworking? You mentioned method swizzling but can you point me to where to start looking to see how this is happening? The session manager I'm using is an AFHTTPSessionManager and I pass it the default configuration:

[NSURLSessionConfiguration defaultConfiguration];

I make sure to setup the session configuration after I've added the protocol classes, so that isn't the issue either.

I'm using swift and objc interchangeably because my project has both and the code snippets come from different files.

@Amindv1
Copy link
Author

Amindv1 commented Dec 19, 2016

Alright so I took a look at OHHTTPStubs+NSURLSessionConfiguration and see where the method swizzling is happening. Now I understand my error. I was thinking that the URLProtocol RegisterClass was what was adding OHHTTPStubs to the session configuration but this isn't the case, it's being 'swizzled' in. Thanks for pointing that out in your comment, it took me a couple of reads through to understand exactly what you meant and I also had to read through the implementation and on method swizzling. Here's the post I read if anyone else that stumbles upon this is as confused as I was:

http://nshipster.com/method-swizzling/

@AliSoftware
Copy link
Owner

AliSoftware commented Dec 19, 2016

Yup that's it.

Again, it's explained explicitly here ("Automatic Loading" paragraph) in the README as pointed out before.

@Amindv1
Copy link
Author

Amindv1 commented Dec 19, 2016

Yea I read through that a bunch of times actually but since I'm a bit of a noob I didn't understand exactly what you meant by swizzling. I thought that the registerclass was what was swizzling the method because I didn't understand the concept of swizzling. I think all the new terminology threw me off too so I was confused when reading through the readme.

Again I really appreciate the help!

@AliSoftware
Copy link
Owner

Ah right. Maybe we should put that NSHipster's link next to the "swizzling" mention in the README then.

@Amindv1
Copy link
Author

Amindv1 commented Dec 19, 2016

Maybe that and mentioning where you're swizzling the sessions, what really did it for me is actually seeing where you guys swizzled the config.

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

No branches or pull requests

2 participants