|
| 1 | +# HTTP Date Format |
| 2 | + |
| 3 | +* Proposal: [SF-0016](0016-http-date-format.md) |
| 4 | +* Authors: [Cory Benfield](https://github.com/Lukasa), [Tobias](https://github.com/t089), [Tony Parker](https://github.com/parkera) |
| 5 | +* Review Manager: TBD |
| 6 | +* Status: **Awaiting Review** |
| 7 | +* Review: ([Pitch](https://forums.swift.org/t/pitch-http-date-format-style/76783)) |
| 8 | +* Implementation: [swiftlang/swift-foundation#1127](https://github.com/swiftlang/swift-foundation/pull/1127) |
| 9 | + |
| 10 | +## Introduction |
| 11 | + |
| 12 | +The HTTP specification [requires that all HTTP servers send a `Date` header field](https://www.rfc-editor.org/rfc/rfc9110.html#field.date) that contains the date and time at which a message was originated in a [specific format](https://www.rfc-editor.org/rfc/rfc9110.html#http.date). This proposal adds support to `FoundationEssentials` to generate this "HTTP" date format and to parse it from a `String`. |
| 13 | + |
| 14 | +## Motivation |
| 15 | + |
| 16 | +The HTTP date format is used throughout HTTP to represent instants in time. The format is specified entirely in [RFC 9110 § 5.6.7](https://www.rfc-editor.org/rfc/rfc9110.html#http.date). The format is simple and static, emitting a textual representation in the UTC time zone. |
| 17 | + |
| 18 | +This format is used frequently across the web. Providing a high-performance and standard implementation of this transformation for Swift will enable developers on both the client and the server to easily handle this header format. |
| 19 | + |
| 20 | +## Proposed solution |
| 21 | + |
| 22 | +We propose to add two additional format styles to `FoundationEssentials` . This formatter would follow the API shape of `ISO8601FormatStyle`. The two styles are: |
| 23 | + |
| 24 | +| Name | Input | Output | |
| 25 | +| ---- | ----- | ------ | |
| 26 | +| Date.HTTPFormatStyle | String | Date | |
| 27 | +| DateComponents.HTTPFormatStyle | String | DateComponents | |
| 28 | + |
| 29 | +Both styles allow formatting (creating a `String`) and parsing (taking a `String`). |
| 30 | + |
| 31 | +A principal design goal is to ensure that it is very cheap to parse and serialize these header formats. Thefore, the implementation is focused on performance and safety. |
| 32 | + |
| 33 | +## Detailed design |
| 34 | + |
| 35 | +The parser requires the presence of all fields, with the exception of the weekday. If the weekday is present, then it is validated as being one of the specified values (for example, `Mon`, `Tue`, etc.), but is ignored for purposes of creating the actual `Date`. For the hour, minute, and second fields, the parser validates the values are within the ranges defined in the spec. Foundation does not support leap seconds, so values of 60 for the seconds field are set to 0 instead. |
| 36 | + |
| 37 | +### Date parsing |
| 38 | + |
| 39 | +`Date.HTTPFormatStyle` will add the following new API surface: |
| 40 | + |
| 41 | +```swift |
| 42 | +@available(FoundationPreview 6.2, *) |
| 43 | +extension Date { |
| 44 | + /// Options for generating and parsing string representations of dates following the HTTP date format |
| 45 | + /// from [RFC 9110 § 5.6.7](https://www.rfc-editor.org/rfc/rfc9110.html#http.date). |
| 46 | + public struct HTTPFormatStyle : Sendable, Hashable, Codable { |
| 47 | + public init() |
| 48 | + public init(from decoder: any Decoder) throws |
| 49 | + public func encode(to encoder: any Encoder) throws |
| 50 | + public func hash(into hasher: inout Hasher) |
| 51 | + public static func ==(lhs: HTTPFormatStyle, rhs: HTTPFormatStyle) -> Bool |
| 52 | + } |
| 53 | +} |
| 54 | + |
| 55 | +@available(FoundationPreview 6.2, *) |
| 56 | +extension Date.HTTPFormatStyle : FormatStyle { |
| 57 | + public typealias FormatInput = Date |
| 58 | + public typealias FormatOutput = String |
| 59 | + public func format(_ value: Date) -> String |
| 60 | +} |
| 61 | + |
| 62 | +@available(FoundationPreview 6.2, *) |
| 63 | +public extension FormatStyle where Self == Date.HTTPFormatStyle { |
| 64 | + static var http: Self |
| 65 | +} |
| 66 | + |
| 67 | +@available(FoundationPreview 6.2, *) |
| 68 | +extension Date.HTTPFormatStyle : ParseStrategy { |
| 69 | + public func parse(_ value: String) throws -> Date |
| 70 | +} |
| 71 | + |
| 72 | +@available(FoundationPreview 6.2, *) |
| 73 | +extension Date.HTTPFormatStyle: ParseableFormatStyle { |
| 74 | + public var parseStrategy: Self |
| 75 | +} |
| 76 | + |
| 77 | +@available(FoundationPreview 6.2, *) |
| 78 | +extension ParseableFormatStyle where Self == Date.HTTPFormatStyle { |
| 79 | + public static var http: Self |
| 80 | +} |
| 81 | + |
| 82 | +@available(FoundationPreview 6.2, *) |
| 83 | +extension ParseStrategy where Self == Date.HTTPFormatStyle { |
| 84 | + public static var http: Self |
| 85 | +} |
| 86 | + |
| 87 | +@available(FoundationPreview 6.2, *) |
| 88 | +extension Date.HTTPFormatStyle : CustomConsumingRegexComponent { |
| 89 | + public typealias RegexOutput = Date |
| 90 | + public func consuming(_ input: String, startingAt index: String.Index, in bounds: Range<String.Index>) throws -> (upperBound: String.Index, output: Date)? |
| 91 | +} |
| 92 | + |
| 93 | +@available(FoundationPreview 6.2, *) |
| 94 | +extension RegexComponent where Self == Date.HTTPFormatStyle { |
| 95 | + /// Creates a regex component to match a RFC 9110 HTTP date and time, such as "Sun, 06 Nov 1994 08:49:37 GMT", and capture the string as a `Date`. |
| 96 | + public static var http: Date.HTTPFormatStyle |
| 97 | +} |
| 98 | +``` |
| 99 | + |
| 100 | +#### DateComponents parsing |
| 101 | + |
| 102 | +The components based parser is useful if the caller wishes to know each value in the string. The time zone of the result is set to `.gmt`, per the spec. The components can be converted into a `Date` using the following code, if desired: |
| 103 | + |
| 104 | +```swift |
| 105 | +let parsed = try? DateComponents(myString, strategy: .http) // type is DateComponents? |
| 106 | +let date = Calendar(identifier: .gregorian).date(from: parsed) // type is Date? |
| 107 | +``` |
| 108 | + |
| 109 | +`DateComponents.HTTPFormatStyle` will add the following new API surface: |
| 110 | + |
| 111 | +```swift |
| 112 | +@available(FoundationPreview 6.2, *) |
| 113 | +extension DateComponents { |
| 114 | + /// Options for generating and parsing string representations of dates following the HTTP date format |
| 115 | + /// from [RFC 9110 § 5.6.7](https://www.rfc-editor.org/rfc/rfc9110.html#http.date). |
| 116 | + public struct HTTPFormatStyle : Sendable, Hashable, Codable { |
| 117 | + public init() |
| 118 | + public init(from decoder: any Decoder) throws |
| 119 | + public func encode(to encoder: any Encoder) throws |
| 120 | + public func hash(into hasher: inout Hasher) |
| 121 | + public static func ==(lhs: HTTPFormatStyle, rhs: HTTPFormatStyle) -> Bool |
| 122 | + } |
| 123 | +} |
| 124 | + |
| 125 | +@available(FoundationPreview 6.2, *) |
| 126 | +extension DateComponents.HTTPFormatStyle : FormatStyle { |
| 127 | + public typealias FormatInput = DateComponents |
| 128 | + public typealias FormatOutput = String |
| 129 | + public func format(_ value: DateComponents) -> String |
| 130 | +} |
| 131 | + |
| 132 | +@available(FoundationPreview 6.2, *) |
| 133 | +public extension FormatStyle where Self == DateComponents.HTTPFormatStyle { |
| 134 | + static var httpComponents: Self |
| 135 | +} |
| 136 | + |
| 137 | +@available(FoundationPreview 6.2, *) |
| 138 | +extension DateComponents.HTTPFormatStyle : ParseStrategy { |
| 139 | + public func parse(_ value: String) throws -> DateComponents |
| 140 | +} |
| 141 | + |
| 142 | +@available(FoundationPreview 6.2, *) |
| 143 | +extension DateComponents.HTTPFormatStyle: ParseableFormatStyle { |
| 144 | + public var parseStrategy: Self |
| 145 | +} |
| 146 | + |
| 147 | +@available(FoundationPreview 6.2, *) |
| 148 | +extension ParseableFormatStyle where Self == DateComponents.HTTPFormatStyle { |
| 149 | + public static var httpComponents: Self |
| 150 | +} |
| 151 | + |
| 152 | +@available(FoundationPreview 6.2, *) |
| 153 | +extension ParseStrategy where Self == DateComponents.HTTPFormatStyle { |
| 154 | + public static var httpComponents: Self |
| 155 | +} |
| 156 | + |
| 157 | +@available(FoundationPreview 6.2, *) |
| 158 | +extension DateComponents.HTTPFormatStyle : CustomConsumingRegexComponent { |
| 159 | + public typealias RegexOutput = DateComponents |
| 160 | + public func consuming(_ input: String, startingAt index: String.Index, in bounds: Range<String.Index>) throws -> (upperBound: String.Index, output: DateComponents)? |
| 161 | +} |
| 162 | + |
| 163 | +@available(FoundationPreview 6.2, *) |
| 164 | +extension RegexComponent where Self == DateComponents.HTTPFormatStyle { |
| 165 | + /// Creates a regex component to match a RFC 9110 HTTP date and time, such as "Sun, 06 Nov 1994 08:49:37 GMT", and capture the string as a `DateComponents`. |
| 166 | + public static var httpComponents: DateComponents.HTTPFormatStyle |
| 167 | +} |
| 168 | +``` |
| 169 | + |
| 170 | +The extensions on the protocols must use a different name for the `DateComponents` and `Date` versions in order to ambiguity when the return type is not specified, such is in `Regex`'s builder syntax. |
| 171 | + |
| 172 | +### DateComponents Additions |
| 173 | + |
| 174 | +The `DateComponents.HTTPDateFormatStyle` type is the first `FormatStyle` for `DateComponents`. Therefore, a few additions are also needed to the `DateComponents` type as well to allow formatting it directly. These are identical to the existing methods on other formatted types, including `Date`. |
| 175 | + |
| 176 | +```swift |
| 177 | +@available(FoundationPreview 6.2, *) |
| 178 | +extension DateComponents { |
| 179 | + /// Converts `self` to its textual representation. |
| 180 | + /// - Parameter format: The format for formatting `self`. |
| 181 | + /// - Returns: A representation of `self` using the given `format`. The type of the representation is specified by `FormatStyle.FormatOutput`. |
| 182 | + public func formatted<F: FormatStyle>(_ format: F) -> F.FormatOutput where F.FormatInput == DateComponents |
| 183 | + |
| 184 | + // Parsing |
| 185 | + /// Creates a new `Date` by parsing the given representation. |
| 186 | + /// - Parameter value: A representation of a date. The type of the representation is specified by `ParseStrategy.ParseInput`. |
| 187 | + /// - Parameters: |
| 188 | + /// - value: A representation of a date. The type of the representation is specified by `ParseStrategy.ParseInput`. |
| 189 | + /// - strategy: The parse strategy to parse `value` whose `ParseOutput` is `DateComponents`. |
| 190 | + public init<T: ParseStrategy>(_ value: T.ParseInput, strategy: T) throws where T.ParseOutput == Self { |
| 191 | + |
| 192 | + /// Creates a new `DateComponents` by parsing the given string representation. |
| 193 | + @_disfavoredOverload |
| 194 | + public init<T: ParseStrategy, Value: StringProtocol>(_ value: Value, strategy: T) throws where T.ParseOutput == Self, T.ParseInput == String |
| 195 | +} |
| 196 | +``` |
| 197 | + |
| 198 | +### Detecting incorrect components |
| 199 | + |
| 200 | +For fields like the weekday, day number and year number, the style will make a best effort at parsing a sensible date out of the values in the string. If the caller wishes to validate the result matches the values found in the string, they can use the `DateComponents` parser, generate the date, then validate the weekday of the result versus the value of the `weekday` field. See _Validating the weekday_ in the *Alternatives Considered* section for more information about why validation is not the default behavior. |
| 201 | + |
| 202 | +The following example test code demonstrates how this might be done: |
| 203 | + |
| 204 | +```swift |
| 205 | +// This date will parse correctly, but of course the value of 99 does not correspond to the actual day. |
| 206 | +let strangeDate = "Mon, 99 Jan 2025 19:03:05 GMT" |
| 207 | +let date = try XCTUnwrap(Date(strangeDate, strategy: .http)) |
| 208 | +let components = try XCTUnwrap(DateComponents(strangeDate, strategy: .http)) |
| 209 | + |
| 210 | +let actualDay = Calendar(identifier: .gregorian).component(.day, from: date) |
| 211 | +let componentDay = try XCTUnwrap(components.day) |
| 212 | +XCTAssertNotEqual(actualDay, componentDay) |
| 213 | +``` |
| 214 | + |
| 215 | +## Source compatibility |
| 216 | + |
| 217 | +There is no impact on source compatibility. This is entirely new API. |
| 218 | + |
| 219 | +## Implications on adoption |
| 220 | + |
| 221 | +This feature can be freely adopted and un-adopted in source code with no deployment constraints and without affecting source compatibility. On Darwin platforms, the feature is aligned with FoundationPreview 6.2 availability. |
| 222 | + |
| 223 | +## Future directions |
| 224 | + |
| 225 | +The HTTP format is exceedingly simple, so it is highly unlikely that any new features or API surface will be added to this format. |
| 226 | + |
| 227 | +## Alternatives considered |
| 228 | + |
| 229 | +### A custom interface instead of a format style |
| 230 | + |
| 231 | +As the HTTP date format is extremely simple and frequently used in performance-sensitive contexts, we could have chosen to provide a very specific function instead of a general purpose date format. This would have the advantage of "funneling" developers towards the highest performance versions of this interface. |
| 232 | + |
| 233 | +This approach was rejected as being unnecessarily restrictive. While it's important that this format can be accessed in a high performance way, there is no compelling reason to avoid offering a generic format style. There are circumstances in which users may wish to use this date format as an offering in other contexts. |
| 234 | + |
| 235 | +### Validating the weekday |
| 236 | + |
| 237 | +The parser could validate the correctness of the weekday value. However, this requires an additional step of decomposing the produced date into components. The authors do not feel this is important enough to warrant the permanent performance penalty, and therefore have provided an alternate method to do so using the components parser in cases where it is required. |
0 commit comments