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

Date decoding in URL query parameters #2481

Closed
letatas opened this issue Aug 26, 2020 · 6 comments
Closed

Date decoding in URL query parameters #2481

letatas opened this issue Aug 26, 2020 · 6 comments

Comments

@letatas
Copy link

letatas commented Aug 26, 2020

Hello, I am trying to retrieve a date from a query parameter encoded in IS8601:

/resources?since=2020-08-18T12:00:00

Unfortunately it seems that the Date in this format is not recognized.

Steps to reproduce

Create a new project vapor new date-issue
In the routes.swift add these lines :

app.get("resources") { req -> String in
    let date = req.query[Date.self, at: "since"]
    return date.debugDescription
}

Run the project, and in your terminal, call:

curl "127.0.0.1:8080/resources?since=2020-08-18T12:00:00"

Expected behavior

I would expect date to be nil only when the parameter since is absent or badly formatted. I expect date to contain the IS8601 decoded value of the since parameter.

Actual behavior

Whatever the parameter since is populated or not, date is always nil, even when a correctly encoded date is set.

Environment

  • Vapor Framework version: 4.29.0
  • Vapor Toolbox version: 12.2.2
  • OS version: macOS Catalina 10.15.6 (19G2021)
@avario
Copy link

avario commented Aug 27, 2020

I think for URL encoded parameters the default date decoding strategy is secondsSince1970 not iso8601. You can change this by modifying the ContentConfiguration.

let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601

ContentConfiguration.global.use(decoder: decoder, for: .urlEncodedForm)

See: https://docs.vapor.codes/4.0/content/#override-defaults

@letatas
Copy link
Author

letatas commented Aug 27, 2020

Thanks @avario for your answer. Unfortunately, I didn't manage to make it work. I set the content configuration in the configure method, but the route still returns nil 🤷‍♂️

@avario
Copy link

avario commented Sep 25, 2020

@letatas Sorry, it should look like this for what you want:

let decoder = URLEncodedFormDecoder(configuration: .init(dateDecodingStrategy: .iso8601))
ContentConfiguration.global.use(urlDecoder: decoder)

@Craz1k0ek
Copy link
Contributor

According to Swift, the date you're passing is invalid. As an example using an example struct.

struct Example: Codable {
    let date: Date
}

First we'll try to decode the time string you provide.

let input = "{\"date\": \"2020-08-18T12:00:00\"}".data(using: .utf8)!
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601

do {
    let e = try decoder.decode(Example.self, from: input)
    print(e)
} catch {
    print(error)
}

You can see how the JSON object is created. This will always print an error.

dataCorrupted(Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "date", intValue: nil)], debugDescription: "Expected date string to be ISO8601-formatted.", underlyingError: nil))

So clearly, Swift doesn't understand what you're providing. So, what is it Swift does expect. Well, let's try the other way around.

let formatter = DateFormatter()
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
let date = formatter.date(from: "2020-08-18T12:00:00")!

let encoder = JSONEncoder()
encoder.dateEncodingStrategy = .iso8601

do {
    let e = Example(date: date)
    let output = try encoder.encode(e)
} catch {
    print(error)
}

If we were to print output as String, we would get

{"date":"2020-08-18T10:00:00Z"}

This might seem a bit odd, but I'm in a different time zone, which is automatically adjusted. This does however show your problem, you're missing the Z at the end.

So fixing your issue, replace the time you're providing with 2020-08-18T10:00:00Z and it should work!
Note: ISO 8601 also specifies a standard using the fractional seconds, which isn't supported by default either. You can use below extension if you were to need it.

extension JSONDecoder.DateDecodingStrategy {
    
    /// The strategy that formats dates according to the ISO 8601 standard.
    /// - Note: This includes the fractional seconds, unlike the standard `.iso8601`, which fails to decode those.
    static var iso8601withFractionalSeconds: JSONDecoder.DateDecodingStrategy {
        JSONDecoder.DateDecodingStrategy.custom { (decoder) in
            let singleValue = try decoder.singleValueContainer()
            let dateString  = try singleValue.decode(String.self)
            let formatter = ISO8601DateFormatter()
            formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
            
            guard let date = formatter.date(from: dateString) else {
                throw DecodingError.dataCorruptedError(in: singleValue, debugDescription: "Failed to decode string to ISO 8601 date.")
            }
            return date
        }
    }
    
}

@seriyvolk83
Copy link

seriyvolk83 commented Nov 7, 2020

avario's answer is correct, but in case you need a custom decoder, then you can use the following:

        let f = DateFormatter()
        f.dateFormat = "yyyy-MM-dd" 

        // Date encoder for GET parameters
        ContentConfiguration.global.use(urlDecoder: URLEncodedFormDecoder.createDateDecoder(formatter: f))

...

extension URLEncodedFormDecoder {
    
    /** Creates decoder with custom date formatter
     - Parameter formatter: the formatter
     
     Example:
     ```
     let f = DateFormatter()
     f.dateFormat = "yyyy-MM-dd"
     
     // Date encoder for GET parameters
     ContentConfiguration.global.use(urlDecoder: URLEncodedFormDecoder.createDateDecoder(formatter: f))
     ```
     */
    static func createDateDecoder(formatter: DateFormatter) -> URLQueryDecoder {
        let urlDecoder: URLQueryDecoder = URLEncodedFormDecoder(configuration: .init(dateDecodingStrategy: .custom({ (d: Decoder) -> Date in
            let string = try d.singleValueContainer().decode(String.self)
            if let date = formatter.date(from: string) {
                return date
            }
            throw Abort(HTTPResponseStatus.badRequest)
        })))
        return urlDecoder
    }
}

@0xTim
Copy link
Member

0xTim commented Nov 18, 2020

Closing as the answer is #2481 (comment)

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

No branches or pull requests

5 participants