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

Use URLSession to send XMLRPC requests #719

Merged
merged 9 commits into from
Feb 6, 2024

Conversation

crazytonyli
Copy link
Contributor

@crazytonyli crazytonyli commented Feb 5, 2024

Description

This PR updates the XMLRPC API client to send HTTP requests using URLSession, when useURLSession is set to true (it's false by default).

Error handling

This PR also changes the error handling in XMLPRC API and the API it surfaces.

When using Alamofire, in most cases, the XMLRPC API client returns NSError instances (plain and simple NSError instance, not the ones that are cast from Swift errors). It may return URLError too. It's quite different from the WP.com REST API client, which returns NSError instances that are cast from Swift errors, which is why I implemented a custom NSError bridge logic.

Since there is no "bridging" happening in WordPressOrgXMLRPCApi, I didn't reuse the custom error bridging code. Instead, I re-used the existing function that creates new NSError instances, for the exiting closure-based APIs. The new async function call(method:parameters:...) returns WordPressAPIError<WordPressOrgXMLRPCApiFault> error instances. That means, depending on which method you call, you'll get different kinds of errors.

The new implementation is in the decodeXMLRPCResult and asNSError functions, which together should work exactly like the existing handleResponseWithData function. And this "non-change" should be covered by the existing error case unit tests.

Testing Details

Similar to #720, the unit tests pass when useURLSession is set to true.

wordpress-mobile/WordPress-iOS#22540 can be used to get an app build which enables useURLSession by default.


  • Please check here if your pull request includes additional test coverage.
  • I have considered if this change warrants release notes and have added them to the appropriate section in the CHANGELOG.md if necessary.

@crazytonyli crazytonyli force-pushed the wordpress-xmlrpc-api-error-new-impl branch from e617373 to 8829f1a Compare February 5, 2024 01:23
@crazytonyli crazytonyli force-pushed the wordpress-xmlrpc-api-error-new-impl branch from 8829f1a to 962d56d Compare February 5, 2024 01:43
@crazytonyli crazytonyli marked this pull request as ready for review February 5, 2024 02:12
Comment on lines 206 to 207
// URLSession-backed API: the error returned by the new one may has HTTP response body which may not
// be the case exist in the old API. I think this is an acceptable change.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// URLSession-backed API: the error returned by the new one may has HTTP response body which may not
// be the case exist in the old API. I think this is an acceptable change.
// URLSession-backed API: the error returned by the new one has an HTTP response body which is not
// the case in the old API. I think this is an acceptable change.

I think it's an acceptable change, too. In particular because the new way has more information than the old. If it was the other way around, then one would have to consider if the loss of information is dangerous for the client. But luckily that's not our case 😄

Copy link
Contributor

@mokagio mokagio left a comment

Choose a reason for hiding this comment

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

I thought about suggesting to duplicated the tests and have one version run with useURLSession true and the other with false.

This might be achieved without lots of copy-paste by running each test via something like [true, false].each { useURLSession in ... }.

But, I expect using URLSession will soon become the only way to use the API. In which case, we might as well save the time to modify the tests and move on with the Alamofire removal work.

Comment on lines 289 to 291
/// Unlike the closed-based APIs, this method returns a concrete error type. You should consider handle the errors
/// as they are, instead of casing them to `NSError` instance. But in case you do need to cast them to `NSError`,
/// considering using the `asNSError` function if you need backward compatiblity with existing code,
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// Unlike the closed-based APIs, this method returns a concrete error type. You should consider handle the errors
/// as they are, instead of casing them to `NSError` instance. But in case you do need to cast them to `NSError`,
/// considering using the `asNSError` function if you need backward compatiblity with existing code,
/// Unlike the closure-based APIs, this method returns a concrete error type. You should consider handling the errors
/// as they are, instead of casting them to `NSError` instance. But in case you do need to cast them to `NSError`,
/// considering using the `asNSError` function if you need backward compatibility with existing code.

I only noticed the typo in compatibility when I loaded the text in this textarea in GitHub and the spell checker picked it up.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

🙈 Thanks! Updated in bcf8ea9

/// considering using the `asNSError` function if you need backward compatiblity with existing code,
///
/// - Parameters:
/// - streaming: set to true if there are large data (i.e. uploading files) in given `parameters`. `false` by default.
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
/// - streaming: set to true if there are large data (i.e. uploading files) in given `parameters`. `false` by default.
/// - streaming: set to `true` if there are large data (i.e. uploading files) in given `parameters`. `false` by default.

"false" at the end is code fenced, so "true" should be, too.

Comment on lines +394 to +396
userInfo[Self.WordPressOrgXMLRPCApiErrorKeyData as String] = data
userInfo[Self.WordPressOrgXMLRPCApiErrorKeyDataString as String] = NSString(data: data, encoding: String.Encoding.utf8.rawValue)
userInfo[Self.WordPressOrgXMLRPCApiErrorKeyStatusCode as String] = statusCode
Copy link
Contributor

Choose a reason for hiding this comment

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

I was surprised by the as String so I drilled into the type definition. All those keys are NSError.UserInfoKey which is an NSString.

I wonder why Apple decided to define userInfo keys as NSString when other similar entities are Strings:

image

Copy link
Contributor

Choose a reason for hiding this comment

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

Side note: From the quick look I had at the code, I got the impression all usages ended up cast as String.

Unless I'm wrong, we could followup with a simplification PR that defines those keys as String.

Comment on lines 511 to 514
var response: HTTPAPIResponse<Data>

var code: Int?
var message: String?
Copy link
Contributor

Choose a reason for hiding this comment

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

Do these need to be mutable?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in c4f376d

}
}

if ["application/xml", "text/xml"].filter({ (type) -> Bool in return contentType.hasPrefix(type)}).count == 0 {
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
if ["application/xml", "text/xml"].filter({ (type) -> Bool in return contentType.hasPrefix(type)}).count == 0 {
if ["application/xml", "text/xml"].filter({ (type) -> Bool in return contentType.hasPrefix(type)}).isEmpty {

From SwiftLint, I learned that isEmpty is more efficient than count == 0. I'm pretty sure the difference is not something we would notice at runtime, but I still think it's a good practice to use isEmpty.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I have simplified this code in aeb9bec

Comment on lines +550 to +562
return .failure(.unparsableResponse(response: response.response, body: response.body))
}

guard !decoder.isFault() else {
return .failure(.endpointError(.init(response: response, code: decoder.faultCode(), message: decoder.faultString())))
}

if let decoderError = decoder.error() {
return .failure(.unparsableResponse(response: response.response, body: response.body, underlyingError: decoderError))
}

guard let responseXML = decoder.object() else {
return .failure(.unparsableResponse(response: response.response, body: response.body))
Copy link
Contributor

Choose a reason for hiding this comment

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

Nitpick: The case were the decoding fails because the decoder has no object and the case where the decoder could not be created call failure with the same error.

I don't think this is a major problem because I doubt at runtime we'll ever run into a case where decoder.error is nil and so is decoder.object.

case let .endpointError(fault):
error = NSError(domain: WPXMLRPCFaultErrorDomain, code: fault.code ?? 0, userInfo: [NSLocalizedDescriptionKey: fault.message].compactMapValues { $0 })
data = fault.response.body
statusCode = nil
Copy link
Contributor

Choose a reason for hiding this comment

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

Would this work?

Suggested change
statusCode = nil
statusCode = fault.response.statusCode

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Depending on what you mean by "work" 😄 . It sure makes sense to me to provide HTTP status code when it's available. However, in the existing implementation, only the status code is only set when it's 400 - 600. I'm just being super cautious to try keeping the existing behaviour changed when possible.

Copy link
Contributor

Choose a reason for hiding this comment

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

Depending on what you mean by "work" 😄 .

Ahah. Good point @crazytonyli . Makes sense to be cautious 👍

Besides, ideally the HTTP level details should remain at the networking library level, and the app should only need to check the typed errors it gets, without needing to look into HTTP error codes.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

the app should only need to check the typed errors it gets, without needing to look into HTTP error codes.

Unfortunately that is not the case at the moment (here is an example), which is one of the reasons that I'd like to be cautious.

@crazytonyli crazytonyli enabled auto-merge February 6, 2024 20:13
@crazytonyli crazytonyli merged commit d0aa9ba into trunk Feb 6, 2024
6 checks passed
@crazytonyli crazytonyli deleted the wordpress-xmlrpc-api-error-new-impl branch February 6, 2024 20:17
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