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

Body of request never makes it to stubRequestPassingTest: #52

Closed
jpalten opened this issue Feb 6, 2014 · 32 comments
Closed

Body of request never makes it to stubRequestPassingTest: #52

jpalten opened this issue Feb 6, 2014 · 32 comments

Comments

@jpalten
Copy link

jpalten commented Feb 6, 2014

I use OHHTTPStubs to test if my AFHTTPRequest send the correct xml as body in a request. This works fine and all tests pass, checking the body that would be sent in the stubRequestPassingTest:

Now I moved to using AFHTTPSessionManager, and the program still works fine, but the tests fail, telling me that the body to be sent is empty, instead of the expected xml.

Something funny that happens: the request that gets prepared by my program has a different pointer (0xe1efbc0) than the request (0xe2c3310) that is checked in the stub. Any idea what is going on? Is it possible the requests gets copied, but the body didn't make it to the copy?

Here is a log from my console:

2014-02-06 14:09:36.278 xctest[69495:303] Posting request 0xe1efbc0 with body:
2014-02-06 14:09:36.278 xctest[69495:1903] Stubbing url /admin/_cmdstat.jsp
2014-02-06 14:09:36.278 xctest[69495:1903] checking body of request 0xe2c3310
2014-02-06 14:09:36.279 xctest[69495:1903] Would sent body:
/Volumes/InternalHD/Projecten/ZoneDirectorViewer/ZoneDirectorViewer/Connection/ServerConnectionTests.m:551: error: -[ServerConnectionTests testGetSystemInfo] : ((requestedBody) equal to (expectedBody)) failed: ("") is not equal to ("")

@AliSoftware
Copy link
Owner

Some code or a sample project maybe? More code, more info, etc? Logs are far from enough to help me debug this!

What version of OHHTTPStubs are you using, are you sure you use the latest which fixes issues with automatic support of NSURLSession (thus AFHTTPSessionManager)?

It seems strange anyway regarding your logs that the log says "Posting request 0xe1efbc0 with body:" letting us suspect an empty body, and that your assertion states that both the request body and the expected body are empty "" (and that assertion fails maybe only because of different typing?) I see nowhere in your logs where you sent the XML.

And finally it seems strange that you Unit test your request body (which is what you send, on which OHHTTPStubs does not have any control) instead of your response body (which is what OHHTTPStubs returns when you stubbed the request)?

Anyway, more info is needed for me to help you on this. I'm not even sure that it is related to OHHTTPStubs itself at all.

@jpalten
Copy link
Author

jpalten commented Feb 6, 2014

Thanks for the swift reply.
What I’m trying to do is this: I have a connection class, and I try to make this connection class send something to a server. My test checks to see if the connection class actually sends the correct post body to the server.

I see the log was not complete, sorry about that. I got a new log here:

2014-02-06 16:08:33.293 xctest[73384:303] preparing response for /admin/_cmdstat.jsp
2014-02-06 16:08:33.297 xctest[73384:303] Posting request 0xe1c2540 with body:
2014-02-06 16:08:33.336 xctest[73384:1b03] find stub for request 0x2828e90
2014-02-06 16:08:33.338 xctest[73384:1b03] Stubbing url /admin/_cmdstat.jsp
2014-02-06 16:08:33.340 xctest[73384:1b03] checking body of request 0x2828e90
2014-02-06 16:08:33.341 xctest[73384:1b03] Would sent body:
/Volumes/InternalHD/Projecten/ZoneDirectorViewer/ZoneDirectorViewer/Connection/ServerConnectionTests.m:551: error: -[ServerConnectionTests testGetSystemInfo] : ((requestedBody) equal to (expectedBody)) failed: ("") is not equal to ("")

Here is my test code:

  • (void) testGetSystemInfo
    {
    NSString* requestBody = @"<ajax-request action="getstat" comp="system">";

    NSData* stubbedResponse = [self loadFixtureNamed:@"getstat-system-sysinfo.xml"];
    [self stubHttpRequestTo:@"/admin/_cmdstat.jsp" requestBody:requestBody response:stubbedResponse];

    __weak ServerConnectionTests* weakSelf = self;
    [self.connection getSystemInfo:^(SystemInfo* systemInfo){
    XCTAssertNotNil(systemInfo);
    weakSelf.responseReceived = YES;
    XCTAssertEqualObjects(systemInfo.name, @"ZD-1100-01");
    XCTAssertEqualObjects(systemInfo.versionNum,@"9.5.1.0");
    XCTAssertEqualObjects(systemInfo.model,@"ZD1106" );
    XCTAssertEqualObjects(systemInfo.serial,@"981323000617" );
    XCTAssertEqual(systemInfo.maxAps,6);
    } failure:^(NSURLSessionTask *operation, NSError *error) {
    XCTAssertNil(error,@"get sys info failed wrong");
    }];

    [self waitForResponse];
    XCTAssertTrue(self.responseReceived);
    }

  • (void)stubHttpRequestTo:(NSString )expectedUrlPath requestBody:(NSString)expectedBody response:(NSData *)responseData {

    NSLog(@"preparing response for %@",expectedUrlPath);
    __block BOOL alreadyTested = NO;

    [OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) {
    NSLog(@"Stubbing url %@",request.URL.path);

    XCTAssertEqualObjects(request.URL.path, expectedUrlPath);
    
    if (!alreadyTested) {
        NSLog(@"checking body of request %p",request);
        NSString* requestedBody = [[NSString alloc] initWithData:request.HTTPBody encoding:NSUTF8StringEncoding];
        NSLog(@"Would sent body: %@",requestedBody);
        XCTAssertEqualObjects(requestedBody, expectedBody);
    
        alreadyTested = YES;
    }
    
    return [request.URL.path isEqualToString:expectedUrlPath];
    

    } withStubResponse:^OHHTTPStubsResponse *(NSURLRequest *request) {
    NSDictionary *headers = @{@"Content-Type" : @"application/json"};
    return [OHHTTPStubsResponse responseWithData:responseData statusCode:200 headers:headers];
    }];
    }

Here is the production code:

  • (void)getSystemInfo:(SuccessBlock)success failure:(FailureBlock)failure {
    [self getStatComponent:@"system" elementXml:@"" success:^(SystemInfo * systemInfo) {
    [self.delegate connection:self receivedSystemInformation:systemInfo];
    success(systemInfo);
    } failure:^(NSURLSessionTask * operation, NSError * error) {
    failure(operation,error);
    }];
    }

  • (void)getStatComponent:(NSString_)component elementXml:(NSString_)elementXml success:(SuccessBlock)success failure:(FailureBlock)failure {
    NSString* url = @"admin/_cmdstat.jsp";
    NSString* bodyString = [NSString stringWithFormat:@"<ajax-request action="getstat" comp="%@">%@",component,elementXml];

    [self ajaxCall:url request:bodyString success:success failure:failure];
    }

  • (void)ajaxCall:(NSString *)url request:(NSString *)bodyString success:(SuccessBlock)success failure:(FailureBlock)failure {
    return [self ajaxCall:url request:bodyString parser:nil success:success failure:failure];
    }

  • (void)ajaxCall:(NSString_)url request:(NSString_)bodyString parser:(ListParser*)parser success:(SuccessBlock)success failure:(FailureBlock)failure {

    void (^luckyHandling)(NSURLSessionTask_, NSData_) = ^(NSURLSessionTask operation,NSData responseData) {
    [self.delegate request:operation.currentRequest returnedData:responseData];
    id responseObject = [XmlResponseParser parseXml:responseData usingParser:parser];
    [self.delegate request:operation.currentRequest returnedObject:responseObject];
    if (success) {
    success(responseObject);
    }
    };

    NSData* bodyData = [bodyString dataUsingEncoding:NSUTF8StringEncoding];

    if ([self handledLocally:url requestBody:bodyData usingSuccessHandling:luckyHandling]) {
    return;
    };

    self.httpSession.requestSerializer = [[XmlRequestSerializer alloc] init];
    self.httpSession.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/xml",@"text/xml",nil];
    self.httpSession.responseSerializer = [[XmlResponseSerializer alloc] init];

    [self.httpSession POST:url body:bodyData constructingBodyWithBlock:^(NSMutableURLRequest request) {
    NSString
    sendingBody = [[NSString alloc] initWithData:[request HTTPBody] encoding:NSUTF8StringEncoding];
    DDLogVerbose(@"Posting request %p with body: %@", request, sendingBody);
    } success:^(NSURLSessionTask operation, NSString responseString) {
    DDLogVerbose(@"get xml ok:\n%@",responseString);
    NSData* responseData = [responseString dataUsingEncoding:NSUTF8StringEncoding];
    luckyHandling(operation,responseData);
    } failure:^(NSURLSessionTask *operation, NSError *error) {
    DDLogWarn(@"error! %@", error);
    [self.delegate request:operation.currentRequest failedWithError:error];
    failure(operation, error);
    }];
    }

Op 6 feb. 2014, om 14:46 heeft AliSoftware notifications@github.com het volgende geschreven:

Some code or a sample project maybe? More code, more info, etc? Logs are far from enough to help me debug this!

What version of OHHTTPStubs are you using, are you sure you use the latest which fixes issues with automatic support of NSURLSession (thus AFHTTPSessionManager)?

It seems strange anyway regarding your logs that the log says "Posting request 0xe1efbc0 with body:" letting us suspect an empty body, and that your assertion states that both the request body and the expected body are empty "" (and that assertion fails maybe only because of different typing?) I see nowhere in your logs where you sent the XML.

And finally it seems strange that you Unit test your request body (which is what you send, on which OHHTTPStubs does not have any control) instead of your response body (which is what OHHTTPStubs returns when you stubbed the request)?

Anyway, more info is needed for me to help you on this.


Reply to this email directly or view it on GitHub.

@AliSoftware
Copy link
Owner

I see you still check the uploaded/sent data, not the stubbed response.
OHHTTPStubs is designed to stub network responses, to stub the data that would normally be returned by a distant server to a network request. Not to simulate data upload sent to it.

So this is probably related to this point "OHHTTPStubs don't simulate data upload" already explained in the README.

@jpalten
Copy link
Author

jpalten commented Feb 7, 2014

It could be, but I don’t really want to simulate data upload, I want to check the data that would be posted/uploaded. Would you like me to create a minimum app with unit tests to show what is going on?

@AliSoftware
Copy link
Owner

Yes, or at least post the code of your test case, I can't see how I could help you without seeing a minimum piece of the code you are using.

@jpalten
Copy link
Author

jpalten commented Feb 7, 2014

This should clear things up: https://bitbucket.org/jpalten/subbertesting

@AliSoftware
Copy link
Owner

I just tested your project.

I commented your XCTAssertEqualObjects(requestedBody, expectedBody) and put some breakpoints to see what was going on.

When iOS calls NSURLProtocol's canInitWithRequest: method for the first set of times (as part of the URL Loding System mechanism managed by iOS and the runtime), the request passed as a parameter does not have any HTTPBody (= nil) when called thru an NSURLSession.
Even bypassing AFNetworking and using [[NSURLSession sharedSession] dataTaskWithRequest:req] directly makes +canInitWithRequest: be called with an NSURLRequest with no HTTPBody — whether we use OHHTTPStubs or not (and use a dedicated NSURLProtocol for the test).

So I believe the issue you are experimenting is not related to OHHTTPStubs but to how Apple's NSURLProtocol and NSURLSession works. This leads to NSURLProtocol's +canInitWithRequest: method to be called with a request with an empty HTTPBody, because HTTPBody and HTTPBodyStream properties are handled internally and specifically by Apple for the HTTP protocol (for the specific internal NSURLProtocol originally managing the "http://" scheme by Apple). This seems to be supported by various StackOverflow answers like [url=http://stackoverflow.com/questions/9301611/using-a-custom-nsurlprotocol-with-uiwebview-and-post-requests](this one).

If you put some symbolic breakpoints on Apple's +[NSURLProtocol canInitRequest:] method you can see that this is true regardless of whether you use OHHTTPStubs or not; the request passed to the NSURLProtocol will always have a nil HTTPBody.

I don't really understand why Apple does this, using the HTTPBody only for requests managed by Apple's internal NSURLProtocol to handle http:// scheme but not to custom protocols… maybe you should file a bugreport to Apple directly about it.

@luca-bernardi
Copy link

I encountered the same issue and I confirm that is an Apple's issue. A radar has been filled (http://openradar.appspot.com/15993891) feel free to dupe it.

@AliSoftware
Copy link
Owner

Thanks @lukabernardi

Issue duped in my own bugreport.apple.com ; and maybe the problem is also present on iOS6, when using NSURLConnection (and a custom NSURLProtocol class registered using the +[NSURLProtocol registerClass:] method)?

I guess I can close this issue and mark it as an Apple Bug.

@luca-bernardi
Copy link

In my test this is not happening with NSURLConnection (check out this test project http://cl.ly/TqlD)

@AliSoftware
Copy link
Owner

Damn you're right… and as if it wasn't strange enough, NSURLConnection seems to call +canInitWithRequest: only once but NSURLSession calls it… 3 times 😲

@luca-bernardi
Copy link

Yeah, the 3 times call drive me crazy. I take a look at the stack trace to be understand if it was a mistake of mine but it's seems that is only Apple that likes to mess with us ;)

@jpalten
Copy link
Author

jpalten commented Feb 11, 2014

I actually had to wrap my tests with „alreadySeen” flags… I felt stupid doing it.

Op 11 feb. 2014, om 22:02 heeft Luca Bernardi notifications@github.com het volgende geschreven:

Yeah, the 3 times call drive me crazy. I take a look at the stack trace to be understand if it was a mistake of mine but it's seems that is only Apple that likes to mess with us ;)


Reply to this email directly or view it on GitHub.

@jpalten
Copy link
Author

jpalten commented Feb 11, 2014

Thanks for all the research and feedback. I’ll poke Apple about it.

Jelle

Op 7 feb. 2014, om 17:46 heeft AliSoftware notifications@github.com het volgende geschreven:

I just tested your project.

I commented your XCTAssertEqualObjects(requestedBody, expectedBody) and put some breakpoints to see what was going on.

When iOS calls NSURLProtocol's canInitWithRequest: method for the first set of times (as part of the URL Loding System mechanism managed by iOS and the runtime), the request passed as a parameter does not have any HTTPBody (= nil) when called thru an NSURLSession.

I believe the issue you are experimenting is not related to OHHTTPStubs but to how Apple's NSURLProtocol and NSURLSession works. This leads to NSURLProtocol's +canInitWithRequest: method to be called with a request with an empty HTTPBody, because HTTPBody and HTTPBodyStream properties are handled internally and specifically by Apple for the HTTP protocol (for the specific internal NSURLProtocol originally managing the "http://" scheme by Apple). This seems to be supported by various StackOverflow answers like url=http://stackoverflow.com/questions/9301611/using-a-custom-nsurlprotocol-with-uiwebview-and-post-requests.

If you put some symbolic breakpoints on Apple's +[NSURLProtocol canInitRequest:] method you can see that this is true regardless of whether you use OHHTTPStubs or not; the request passed to the NSURLProtocol will always have a nil HTTPBody.

I don't really understand why Apple does this, using the HTTPBody only for requests managed by Apple's internal NSURLProtocol to handle http:// scheme but not to custom protocols… maybe you should file a bugreport to Apple directly about it.


Reply to this email directly or view it on GitHub.

@AliSoftware
Copy link
Owner

@lukabernardi Actually that's kinda strange that in your example with NSURLConnection it is only called once… because I clearly remember having identified this multi-call of +canInitWithRequest: in the past, even at the beginning of the OHHTTPStubs project (way before iOS7 and NSURLSession existed).

That's actually what made me refactor my API in version 2.0.0 and drove me crazy at that time in the beginning of the project too ;)

If you're interested in some digging in my code, you can see that in the previous API I had an addRequestHandler: method only one block — instead of one for the test and one for the response like now — and I did introduce the onlyCheck parameter at that time because I already saw that the +canInitWithRequest: was called a lot with the same request before startLoading was even called. So the onlyCheck parameter was a way to avoid building the response as many times as canInitWithRequest: was called for nothing…

Now what's strange is that this is now not the case with NSURLConnection any more… but is back with NSURLSession 😲 Or maybe it depends on other mysterious hidden factors…
Sometimes Apple acts in mysterious ways 😆

@luca-bernardi
Copy link

Yep, probably it depends on the underlaying implementation. Do we want to speak about the fact that +[NSURLSession dataTaskWithURL:](and similar) are acting like a class cluster and is returning a private object that is not even a subclass o NSURLSessionTask et al.?

@AliSoftware
Copy link
Owner

;) That's probably related, as every [NSURLSession fooTaskWithBar:] are proably based on the same codebase, all returning an NSURLSessionTask subclass at least… But I'm afraid to wonder how this is implemented under the hood if even with those public subclasses made available, they had the need to do a class cluster and to use private internal classes…

Didn't investigate on what those private subclasses were / looked like, and neither tried and understand why they needed such a design pattern / architecture, but I'm afraid it's related to handle differently requests to internal and external protocols…

I think I'll stop wondering and stop digging, I already went crazy long ago since version 1.0 with the multi-call to canInitWithRequest: 😆

Cheers 🍻

@luca-bernardi
Copy link

Spoiler: the returned objects are all subclass of __NSCFURLSessionTask

Cheers

@AliSoftware
Copy link
Owner

Given the probable toll-free-bridging needed/used to handle all this with CFNetwork internally (I guess), that's not really surprising 😉

@DenTelezhkin
Copy link

Just to add some clarification. This happens only with NSURLSession. This is probably why Apple provides us access to currentRequest and originalRequest on NSURLSessionTasks objects. Seems like HTTPBody of originalRequest gets turned onto HTTPBodyStream on currentRequest.

My solution to this was never checking on HTTPBody inside stubRequestsPassingTest:WithStubResponse: method, but instead provide a separate test, that specifically checks HTTPBody property on NSURLSessionTask originalRequest.

@cwagdev
Copy link

cwagdev commented Jun 13, 2014

I was previously using HTTPBody to determine what type of XML-RPC Payload was sent in order to provide different OHHTTPStubsResponse's

How would I go about doing this now that I can no longer inspect the XML-RPC payload in the stubRequestsPassingTest: block?

@luca-bernardi
Copy link

Personally I'm using a terrible hacking and be aware that I'm definitely not endorsing this and I can do that only in a project where I'm in control of the code that is actually building the NSURLRequest (AKA network library):
I'm basically putting the HTTP body inside a custom HTTP header (X-Debug-HTTPBody), in this way I'm able to extract the header in the stubRequestsPassingTest:.

@cwagdev
Copy link

cwagdev commented Jun 13, 2014

Interesting approach! I could probably just include the XML-RPC method as a header in a similar fashion as it is all that I am interested. Thanks for the idea!

@DenTelezhkin
Copy link

@lukabernardi @cwagdev Better approach would be never checking on HTTPBody in stubRequestsPassingTest: method, and provide a separate test, that creates NSURLSessionDataTask, and checks for originalRequest.HTTPBody.

@cwagdev
Copy link

cwagdev commented Jun 13, 2014

@DenHeadless I think what happened is that I effectively wrote integration tests. I am using OHHTTPStubs to return different XML responses based on the method being invoked. So it is effectively testing my client, xml creation, and xml parsing all together. Probably need to reduce this to actual unit tests instead.

@AliSoftware
Copy link
Owner

Guys, I got another workaround which seems cleaner than adding a dummy header and won't alter your requests:

  • Use [NSURLProtocol setProperty:forKey:inRequest:] to associate whatever data you need to your request (for example the request's initial HTTPBody or directly the request's parameters, or the XML-RPC method or whatnot)
  • Then test it in your stub condition block using [NSURLProtocol propertyForKey:inRequest:].

I just confirmed that it works using @jpalten 's example project.


For those who are using AFNetworking to build the request, one can provide a custom subclass of AFHTTPRequestSerializer that calls super then add a call to [NSURLProtocol setProperty:parameters forKey:@"parameters" inRequest:request] to it in order to query that property in your stubs. Tested that as well and works like a charm.

@AliSoftware
Copy link
Owner

I just wrote a small Wiki Page with all the details about this workaround and some suggestion/example on how to implement it when using AFNetworking to build your requests.


This would allow you to do stuff like this:

[OHHTTPStubs stubRequestsPassingTest:^BOOL(NSURLRequest *request) {
    NSDictionary* params = [NSURLProtocol propertyForKey:@"parameters" inRequest:request];

    // Only stub POST requests to "/login" with "user" = "foo" & "password" = "bar"
    return [request.HTTPMethod isEqualToString:@"POST"] && [request.URL.path isEqualToString:@"/login"]
        && [params[@"user"] isEqualToString:@"foo"] && [params[@"password"] isEqualToString:@"bar"];
} withStubResponse:^OHHTTPStubsResponse *(NSURLRequest *request) {
    return [OHHTTPStubsResponse responseWithData:validLoginResponseData statusCode:200 headers:headers];
}].name = @"foo/bar login";

@luca-bernardi
Copy link

@AliSoftware that's definitely more cleaner solution. Thanks!

@cwagdev
Copy link

cwagdev commented Jun 16, 2014

👍 Thanks @AliSoftware !

@samskiter
Copy link

Hi, not using OHHTTPStubs, but stumbled into this while hitting exactly this issue myself. Found your discussion very useful for diagnosing the problem. Boiled it down and produced a project demonstrating the problem at it's simplest (and the difference between NSURLSession and NSURLConnection) - https://github.com/samskiter/testnsurlprotocol

Thanks for the information everyone and thanks @AliSoftware for the workaround

@AliSoftware
Copy link
Owner

@samskiter thx for the feedback 👍

@AliSoftware
Copy link
Owner

Hi all, this issue should now have a workaround thanks to #166 (which has just been merged into master)!
You'll now be able to use the new OHHTTPStubs_HTTPBody property on NSURLRequest if you want to test your request body in stubs even after iOS cleared the native property now.

Will update the wiki entry and do a release soon.

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

6 participants