Skip to content

Commit 8190cd3

Browse files
authored
[Proposal] HTTP date format (#1132)
* Proposal: HTTP date format * Add DateComponents * Fix up some DateComponents API pieces
1 parent 5f9777d commit 8190cd3

File tree

1 file changed

+237
-0
lines changed

1 file changed

+237
-0
lines changed

Proposals/0016-http-date-format.md

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
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

Comments
 (0)