Pitch: HTTP Date Format Style

Hi folks. There's been considerable interest in being able to support the very common RFC 9110 HTTP date format style in FoundationEssentials, which would make it available cheaply across the Swift ecosystem. This pitch outlines a proposed shape for this API, and I'd love to hear your feedback.


HTTP Date Format

  • Proposal: SF-NNNN
  • Authors: Cory Benfield, Tobias
  • Review Manager: TBD
  • Status: Awaiting implementation

Introduction

The HTTP specification requires that all HTTP servers send a Date header field that contains the date and time at which a message was originated in a specific format. This proposal adds support to FoundationEssentials to generate this "HTTP" date format.

Motivation

The HTTP date format is used throughout HTTP to represent instants in time. The format is specified entirely in RFC 9110 § 5.6.7. The format is simple and static, emitting a textual representation in the UTC time zone.

This format is used heavily across the web, causing it to be parsed and serialized enormously frequently. Providing a standard implementation of this transformation for Swift will enable developers on both the client and the server to easily handle this header format. Because the field is used frequently, there is substantial power and efficiency savings to be gained by having a high-performance parser and serializer for this format.

Proposed solution

We propose to add an additional format style to FoundationEssentials . This formatter would follow the API shape of ISO8601FormatStyle .

The implementation of this format style will be focused on performance and safety. A principal design goal is to ensure that it is very cheap to parse and serialize these header formats.

Detailed design

The HTTP date format will have add the following new API surface:

extension Date {
    public func httpFormat() -> String
}

extension Date {
    /// Options for generating and parsing string representations of dates following the HTTP date format
    /// from [RFC 9110 § 5.6.7](https://www.rfc-editor.org/rfc/rfc9110.html#http.date).
    public struct HTTPFormatStyle : Sendable {
        public init(from decoder: any Decoder) throws

        public func encode(to encoder: any Encoder) throws

        public func hash(into hasher: inout Hasher)

        public static func ==(lhs: HTTPFormatStyle, rhs: HTTPFormatStyle) -> Bool

        public init() { }
    }
}

extension Date.HTTPFormatStyle : FormatStyle {
    public typealias FormatInput = Date
    public typealias FormatOutput = String

    public func format(_ value: Date) -> String
}

public extension FormatStyle where Self == Date.HTTPFormatStyle {
    static var http: Self {
        return Date.HTTPFormatStyle()
    }
}

extension Date.HTTPFormatStyle : ParseStrategy {
    public func parse(_ value: String) throws -> Date
}

extension Date.HTTPFormatStyle: ParseableFormatStyle {
    public var parseStrategy: Self
}

extension ParseableFormatStyle where Self == Date.HTTPFormatStyle {
    public static var http: Self { .init() }
}

extension ParseStrategy where Self == Date.HTTPFormatStyle {
    @_disfavoredOverload
    public static var http: Self { .init() }
}

extension Date.HTTPFormatStyle : CustomConsumingRegexComponent {
    public typealias RegexOutput = Date
    public func consuming(_ input: String, startingAt index: String.Index, in bounds: Range<String.Index>) throws -> (upperBound: String.Index, output: Date)?
}

extension RegexComponent where Self == Date.HTTPFormatStyle {
    /// 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`.
    public static var http: Date.HTTPFormatStyle {
        return Date.HTTPFormatStyle()
    }
}

Source compatibility

There is no impact on source compatibility: this is entirely new API.

Implications on adoption

This feature can be freely adopted and un-adopted in source code with no deployment constraints and without affecting source compatibility.

Future directions

The HTTP format is exceedingly simple, so it is highly unlikely that any new features or API surface will be added to this format.

Alternatives considered

A custom interface instead of a format style

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.

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.

9 Likes

I think this generally makes sense to add, except for the Date extension. There is no precedence for adding a format-specific method to turn a Date into a String, not even for the more common ISO8601 formats. (edit: I'm wrong on this point).

I don't think this is important enough to warrant top-level API additions on Date itself. (Adding a httpFormat() method also begs the question of why there isn't a corresponding Date(httpFormat:) throws initializer, which I also think should not be added)

The rest all seems fine to me. :+1:

I don't think that's true. This API shape matches ISO8601 format essentially exactly, including this method:

extension Date {
    public func ISO8601Format(_ style: ISO8601FormatStyle = .init()) -> String {
        return style.format(self)
    }
}

Wow, you're right, I totally missed that.

Regardless, I don't think this is important enough to elevate to Date's API. A simple someDate.formatted(.http) is sufficient.

1 Like

I appreciate that input @davedelong, thanks. This is an opportunity for us to decide what parts of ISO8601's interface are special to it, and what parts are the convention for special-case formatters, and I think this input is valuable.

1 Like

Like @davedelong I'm also in favor of removing this date extension

extension Date {
    public func httpFormat() -> String
}

I was the author of

extension Date {
    public func ISO8601Format(_ style:) -> String
}

But I wish we didn't have it. It was added before we introduced a family of FormatStyles. There is this generic one already, and I think it should be preferred over the other free-floating ones.

extension Date {
    public func formatted<F: FoundationEssentials.FormatStyle>(_ format: F) -> String
}
14 Likes

+1 for this.

HTTP header parsing/serialization is a common operation in server applications. Having a safe, performant parser is a great addition.

In addition parsing and serialization of http header style dates is commonly one of the few reasons to include Foundation instead of FoundationEssentials in a server based library.

I wonder if it is worthwhile to add an option that supported -0800 style timezones as well. These aren't used in the http header but are commonly used elsewhere.

1 Like

To bikeshed the name a bit, RFC9110FormatStyle would be more consistent with the existing type name and would be a bit more future-proof (who knows, maybe HTTP4 or whatever will switch to POSIX timestamps and then the type name is misleading!)

6 Likes

Reading through this proposal, I’d just assumed RFC 9110 was some kind of highly technical date-time document incorporated by reference into the HTTP specification and also used in other contexts—nope: it’s the HTTP specification itself.

Now, maybe I’m uniquely uninformed, but my guess is that if you polled a hundred developers (even web developers) you’d get many logs more recognition of “HTTP” than “RFC 9110.”

With Swift nomenclature, clarity is the goal here. All else being equal, precision can certainly contribute to clarity. But here the cost to recognition could hardly be more severe, and the benefit in removing ambiguity is entirely hypothetical. For existing parallels, consider that we refer to ASCII encoding, not “ANSI X3.2-1986”; binary floating-point types, not “IEEE 1394-2008”; and so on.

And as to future-proofing—looking at the existing version history, seems more likely that a new document will supersede RFC 9110 as the most current HTTP specification, with the date format unchanged, than that HTTP headers will adopt an alternative date format.

(Did you notice that I referenced the FireWire standard instead of the floating-point standard?)

8 Likes

I actually agree. Maybe HTTPRFC9110 or something similar signaling the format and common pairing. Seems a little messy but might alleviate the pain of discovery. Alternatively aliases dateFormaHTTP1, dateFormatHTTP2 for RFC9110?

RFC9110FormatStyle might be more consistent with ISO8601FormatStyle in name style except ISO8601 defines just a date time format. RFC9110 defines the HTTP protocol and obsoletes a series of RFC 7230 through to 7235 which in turn obsolete RFC2616, which also obsoletes RFC2068. Who is to say in a few years time 9110 is not obsoleted by another RFC which then would mean the name RFC9110 is out of date.

Throughout all these revisions the format of the HTTP date has not changed. And the other thing that has been consistent is the name it has been given HTTP-date.

2 Likes

Really my only question here is whether the FormatStyle APIs are capable of the performance necessary for such a common operation. HTTP dates are something that frameworks that do well at Empower and other benchmarks will literally cheat to produce, that's how common they're a bottleneck in server throughput. I'd hate to see this API get adopted but then immediately shunted aside for a performant version. So unless FormatStyle itself isn't a bottleneck (that is, the bottleneck is strictly in generating the date bytes and so can be heavily optimized against), it seems like these APIs should live in a separate, high performance framework, like the other HTTP types.

5 Likes

This is a real concern of mine as well Jon. I think it’s appropriate for the formatter to live in FoundationEssentials regardless, but I also agree that making this as performant as possible is an explicit goal. Much of that will involve bespoke implementation, rather than falling through to the more flexible formatters available elsewhere.

As a sidebar, much of the cheating involves formatting dates very infrequently. As the HTTP format only has 1s resolution, servers can safely precompute the string once a second and use that result everywhere. Done carefully, this can ensure that even a slightly suboptimal result is still essentially “free” for the server.

2 Likes

Can you clarify what performance concerns you have with FormatStyle? Is it around fetching the localization resources through ICU? We definitely want to keep performance work on top of the list.

FWIW ISO8601FormatStyle does take a special path -- it's implemented without ICU, and thus is available in FoundationEssentials. @Tony_Parker did a bunch of work and measured a pretty good performance improvement recently.

1 Like

Not my most popular suggestion, eh? Well, just wanted to make sure we discussed it. :grimacing:

1 Like

I don't think this is needed. date.formatted(.http) is convenient enough and can be made equally performant if Date.formated(_:) is made @inlinable (and @inline(__always)/@_transparent if needed).

Otherwise looks good to me!

3 Likes

I added a prototype implementation here, with some additions and changes.