0

I'm first going to ask this without full listings and logs because it feels like the sort of thing that people might recognize generically from their own work.

iOS 16 simulator, Xcode 14.2.

Aim

I want to upload a file to a REST server. I'm using URLSessionUploadTask. HTTP Basic authentication goes through (by the low-level fact that once I provide basic creds, URLSession stops asking).

I can assume that the bytes are getting there: My task delegate's urlSession(_:task:didSendBodyData:... is called with the right number of bytes, and equal to the expected number of bytes. I assume that's not a count of what was cast into the net, but the product of some client/server acknowledgment.

The minor odd thing is that I see in my logs is first the server-trust auth challenge, then the did-send, and only then HTTPBasic.

∞ Loop

The major odd thing is:

didReceive challenge: NSURLAuthenticationMethodServerTrust
didSend: 296 / 296
didReceive challenge: NSURLAuthenticationMethodHTTPBasic
didReceive challenge: NSURLAuthenticationMethodServerTrust
didSend: 592 / 592
didReceive challenge: NSURLAuthenticationMethodHTTPBasic

... and so on, ad infinitum, accumulating by multiples of the total data. I admit I have not checked the arithmetic on payload size, I suspect that the count is not necessarily common-sensical. If you have some experience to which the counts are critical, I'm glad to hear from you.

The delegate methods for end-of-transfer, success or failure, are never called. The closure argument for URLSession.shared.dataTask... is never called.

The server's listing page does not show the file present.

Supplement: Multipart

Content-Type: multipart/form-data; boundary=593FBDC3-7A99-415D-B6B4-3F553CB6C9C2
--Boundary-593FBDC3-7A99-415D-B6B4-3F553CB6C9C2
Content-Disposition: form-data; name="file"; filename="InputSample.zip"
Content-Type: application/zip

0123456
--Boundary-593FBDC3-7A99-415D-B6B4-3F553CB6C9C2--

The linebreaks I intend are \r\n, per the general standard. "0123456" is a part of this package as Data containing that string. I wonder if the promise of .zip content without actual .zip-formatted data is a problem. I hadn't thought J. Random Apache would be that "helpful."

Oh, and:

My upload task calls .resume() once and only once. Instruments shows no hotspot or deep stack in my code, which I'd expect in a coded infinite loop.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
Fritz Anderson
  • 409
  • 4
  • 14
  • "didSend: 296 / 296": What do you print there? – Larme Jan 25 '23 at 09:39
  • 1
    Probably unrelated, but the `multipart/form-data` example is not well-formed. First, the boundary in the `Content-Type` is defined as `593FBDC3-7A99-415D-B6B4-3F553CB6C9C2` (and I assume you defined that as a header, though the example makes it look like it is in the body of the request). But in the body of the request, you are using a boundary of `Boundary-593FBDC3-7A99-415D-B6B4-3F553CB6C9C2`. Second, the last boundary should have `--` at the end, but it looks like it is an en-dash (possibly introduced when editing this question?). Make sure you have two hyphens at the end, not an en-dash. – Rob Jan 25 '23 at 17:48
  • @Larme - the numbers come from the task delegate method, the long name being distinguished by `didSendBodyData`. The first number is the number of bytes sent (and, I'd think, acknowledged) and the second is the total amount send so far. Possibly it would be more useful to show …totalBytesExpectedToSend… instead. – Fritz Anderson Jan 25 '23 at 20:00
  • @Rob - I wish I could find an example of what you advise. The only clear one is [here](https://www.donnywals.com/uploading-images-and-forms-to-a-server-using-urlsession/). It suggests putting the first `Content-Type` in the multipart body instead of l`addValue(_:forHTTPHeaderField:)`, which you recommend(?) What I cite states, as grammar, that only the unique content of the boundaries may appear where the boundary is defined. Correct, the em dash is not in the original. A lot of things in the HT* world seem to be accommodations for amateurs (me?) who get it slightly wrong. – Fritz Anderson Jan 25 '23 at 20:11
  • 1
    Yeah, it is complicated. [Alamofire](https://github.com/Alamofire/Alamofire) gets you out of the weeds on this stuff. If you’re looking for examples of manually creating `multipart/form-data` requests, see https://stackoverflow.com/questions/26162616/upload-image-with-parameters-in-swift/26163136#26163136 or, for large assets, avoid `Data` entirely and use file-based upload such as https://stackoverflow.com/questions/70525604/how-to-convert-a-local-video-to-base64-in-swift/70552269#70552269. In short, `Content-Type` is a header, and the boundary in the header must match the body. – Rob Jan 25 '23 at 20:30
  • FWIW, that article you reference is pretty decent. There is a typo where he shows you the boundary header early in the article (where the boundary is just the UUID, but subsequently “Boundary-{uuid}” in the body), but he does it correctly in the code later in the article. But he does say that the `Content-Type` is a “header” and does not say that that this goes into the body. – Rob Jan 25 '23 at 20:44
  • "Possibly it would be more useful to show …totalBytesExpectedToSend… instead" Indeed, that's what I was thinking of. Maybe print the values ? Seeing the doc of that method, the value can differ/be nil. – Larme Jan 25 '23 at 21:00

1 Answers1

0

In terms of why you are seeing a request retried, that is a product of an authentication challenge system. When we receive a challenge, we provide authentication credentials, and URLSession will automatically retry the request with the supplied credentials. It will do this with every authentication challenge, unless we explicitly tell it to cancel.

So, that having been said, I have two observations:

  • Given the ping-ponging between different NSURLAuthenticationMethod values, I suspect there is likely a disconnect between what authentication system the client challenge handler prepared and what the server requested.
  • The infinite loop is likely a result of the client challenge handler not disambiguating between an initial challenge (in which case you should supply the credentials) and the server rejecting the previously supplied credentials (in which case you should just cancel and report the error in the UI).

To answer this definitively, we would need to see:

  • the client authentication challenge handler code; and
  • details about which type of authentication has actually been set up on your server.

But one could easily end up with the pattern you describe if the client ignores the specific authentication type being requested (presumably not “basic”), but proceeds to supply “basic” authentication credentials nonetheless.

Make sure the client’s challenge handler is looking at the protectionSpace, identifying what sort of challenge it is receiving, and that it prepares an authentication response of the appropriate authentication scheme.

Also, you will want to differentiate between a challenge that is requesting credentials for authentication (the first time the delegate method is called) and an authentication that failed (a second, subsequent call to the delegate method). We should provide credentials in response to the former, but cancel in the case of the latter.

See Handling an Authentication Challenge.

Rob
  • 415,655
  • 72
  • 787
  • 1,044
  • Not the full and thoughtful response S/O and you deserve, but to declutter… (# 1) Many things appall me about HTTP basic, but I politics. (# 2) I'd been using protectionSpace: `let method = challenge.protectionSpace.authenticationMethod \… \ guard method == NSURLAuthenticationMethodHTTPBasic \…` (# 3) I now do task.cancel() if the basic challenge's count != 0 (now → 2 rounds). Must I also call into the completion handler; with what? If I'm a presumed intruder (s/b 401), why do I end up with 404, which leaks significant information? (# 4) AF does background Sessions again? – Fritz Anderson Jan 25 '23 at 21:47
  • 1
    You don’t `task.cancel()`. You call the completion handler with `.cancelAuthenticationChallenge` or `.performDefaultHandling`. – Rob Jan 25 '23 at 22:30
  • Per @Rob's advice, I've solved my authentication loop. My employer, by the way, uses a self-signed certificate, which was adding another layer. That one had me override ATS and narrow the server trust challenge to just our domain. That works now, exposing the 404s I get no matter what destination URL I try, and whether AF or not. But that's a secondary issue that shouldn't seep into this one — next stop, yell politely at the server-side developer. Thanks again. – Fritz Anderson Jan 30 '23 at 22:28