Teepee design: Status-Line, take two

My first look at the Status-Line kept largely to what rust-http had done; some great discussion came up and a conceptual flaw in my models was revealed. Now I present some better options.

This article is part of the series on the rust-http redesign, Teepee.

Of the first article, most details are superseded here; the most important part that is still definitely in place is the status code class part.

The criticism and responses to it

The main point of criticism levelled at the designs I posted is that it was still treating the Reason-Phrase too importantly.

Some argue in favour of dropping the reason phrase altogether. That I am not willing to do, citing the 451 example for justification. No, the reason phrase must remain around, somewhere.

A common suggestion is splitting the status code and reason phrase apart. This is looking like a healthy option. It is one that had not occurred to me; I had become too tied up in their correlation, that being what I had been working with in the past in rust-http.

Library designers, beware—​your own perspective will become warped over time so that you often see only slight variations from what you are doing. Getting fresh eyes on a solution is marvellous; I am very glad that I wrote about this matter. Thank you, everyone, for the feedback: Teepee will be the stronger for it.

The new design

I now present a new design, influenced strongly by the feedback received.

At the lowest level, rather than giving a single Status object, the code will work with the pair (StatusCode, ReasonPhrase).

Responses received by the client will have both of these:

struct Response {
    status_code: StatusCode,
    reason_phrase: ~str,
    ...
}

Responses a server is writing will have both fields, but with the reason phrase optional:

struct Response {
    status_code: StatusCode,
    reason_phrase: Option<~str>,
    ...
}

Normally people will just set the status code. If the reason phrase is None, the appropriate value will be inferred, or for unknown statuses, something like “<no reason phrase given>”.

impl StatusCode {
    fn default_reason(&self) -> Option<&'static str> {
        match self.to_u16() {
            100 => Some("Continue"),
            101 => Some("Switching Protocols"),
            102 => Some("Processing"),
            200 => Some("OK"),
            ...,
            _ => None,
        }
    }
}

I am satisfied with this design. Now the only question remaining is how StatusCode itself is represented.

The StatusCode part: a few designs

There are a few possibilities for the Status-Code part; I present here a few, each with its benefits and drawbacks. Their ordering is not significant.

#1: an enum with an extension code variant

pub enum StatusCode {
    Continue,
    SwitchingProtocols,
    Ok,
    ...,
    ExtensionCode(u16),
}

This is similar to what is done in rust-http. It takes four bytes, while the others take two bytes.

This maps extremely well onto the RFC 2616 definition of Status-Code:

      Status-Code    =
            "100"  ; Section 10.1.1: Continue
          | "101"  ; Section 10.1.2: Switching Protocols
          | "200"  ; Section 10.2.1: OK
[many fields omitted for brevity]
          | extension-code

      extension-code = 3DIGIT

However, this has the backwards‐compatibility problem (one I identified earlier but forgot to write down in my previous article) that when a new status 103 Newly Registered Value is registered, things will change from using ExtensionCode(103) to using NewlyRegisteredValue. It’s a very rare case, but very significant.

#2: a 500‐variant C‐style enum

pub enum StatusCode {
    Continue = 100,
    SwitchingProtocols = 101,
    Processing = 102,
    Code103 = 103,
    Code104 = 104,
    ...,
    Ok = 200,
    ...,
    Code599 = 599,
}

This scheme has the slight weakness that when a status becomes registered, its variant will change; this is not, however, a severe backwards‐compatibility problem, for we would leave static aliases behind (e.g. pub static Code103: StatusCode = NewlyRegisteredValue). That could also be used to take care of cases like 451.

#3: a simple wrapping struct with associated statics

I handle this as a separate struct rather than just as u16, because there are only 500 valid values (100–599). Just as ~str applies checks to maintain the invariant that it is valid UTF-8, I will ensure that an illegal StatusCode can never exist. (That applies to the ExtensionCode variant of the enum solution too, by the way.)

pub struct StatusCode {
    code: u16,
}

// 1xx Informational
pub static CONTINUE: Status = Status { code: 100 };
pub static SWITCHING_PROTOCOLS: Status = Status { code: 101 };
pub static PROCESSING: Status = Status { code: 101 };

// 2xx Success
pub static OK: Status = Status { code: 200 };
...

The biggest thing this has going against it is that pattern matching won’t work.

#4: just a number

A variant of #3, one can cause pattern matching to work by using a number type directly:

pub type StatusCode = u16;

I am not in favour of this solution because of semantic mismatch. A status is not just a number—​it has 500 valid values, as mentioned above, and I wish to maintain the invariant. Also things like + simply don’t make sense for a status. Sure, it’s shown as a number, but it is opaque data.

Conclusion

I am now satisfied with the overall design of the Status-Line handling. As to the representation of StatusCode, my current order of preferences, from favourite to least favourite, is #2, #3, #1, #4.

I now invite you to join in the discussion at /r/rust.

Update: the HTTP/2.0 situation

Something I neglected to do all along was refer to the HTTP/2.0 specification. (I looked at that part a few months ago, but had forgotten about it. I’ve been focusing more on the multiplexing aspects of HTTP/2.0, as that’s the main change and the main area of implementation difficulty for Teepee.) It has a couple of notable changes: