~ ✍️
RSS image

« Back to index

Release: http-rest-file Parser

Aug 3 2023

I just released version v0.5.0 of a recursive descent parser I wrote in Rust for the .http and .rest htp client file format. You can find the repository here on github.

Why did I start this project? I am currently working on a larger project for sending http requests and I wanted to store the requests locally on the file system. I thought why not use an existing format for specifying requests and already had some experience with the Jetbrains editor that has an inbuilt editor for sending HTTP requests. Therefore I wrote this recursive descent parser that can parse the request model from the file format as well as serialize an existing model to a file.

The http rest file format describes a request with target, headers and body that can be performed by a api client for testing purposes.

JetBrains has an inbuilt http client within their editor that can perform these specified requests. See here for more information: Intellij Http Syntax

What is the .http/.rest format

With the http file format you can specify requests given

Such a request could look like this:

GET https://httpbin.org/get

or using a POST and some additional meta information in the comments:

### Some comment describing the request 
# @name=Request Name
# @no-log @no-follow
POST https://httpbin.org/post
Content-Type: application/json

< path/to/json/file.json

>>! save_response_output.json

JetBrains also specified the request in editor in the following github project. http-request-in-editor-spec The content of the specification seems to be somewhat outdated but still describes the format quite well.

Add Library Using Cargo

cargo add http-rest-file

Usage

If you are using this crate you want to either parse the content of a .http/.rest file into a model or do the vice versa and create the file content from an existing model.

Model

The request model looks like this:

pub struct Request {
    pub name: Option<String>,
    pub comments: Vec<Comment>,
    pub request_line: RequestLine,
    pub headers: Vec<Header>,
    pub body: RequestBody,
    pub settings: RequestSettings,
    pub pre_request_script: Option<PreRequestScript>,
    pub response_handler: Option<ResponseHandler>,
    pub save_response: Option<SaveResponse>,
}

Some parts of it are contained within the comments (meta information) such as the name as well as the settings (@no-log, @no-cookie-jar, @no-follow). The request_line contains the http method type, url as well as optionally the http version. The pre-request scripts and response handler are optional and do not need to be present. Optionally, the result of a respones can be sent to a file specified by the SaveResponse type.

Parsing Text -> Model

Parse a file given a file path:

use http_rest_file::Parser;
use std::path::PathBuf;

fn main() {
  let model = Parser::parse_file(PathBuf::from("./your/path/request.http")).expect(jj)
}

Parse string content

let str = r#####"
POST http://example.com/api/add
Content-Type: application/json

< ./input.json
###

GET https://example.com/first
###
GET https://example.com/second


###
        "#####;

        let FileParseResult { requests, errs } = dbg!(Parser::parse(str, false));
        println!("errs: {:?}", errs);
        assert_eq!(errs.len(), 1);
        assert_eq!(requests.len(), 3);

        // @TODO check content
        assert_eq!(
            requests,
            vec![
                model::Request {
                    name: None,
                    comments: vec![],
                    headers: vec![Header {
                        key: "Content-Type".to_string(),
                        value: "application/json".to_string()
                    }],
                    body: model::RequestBody::Raw {
                        data: DataSource::FromFilepath("./input.json".to_string())
                    },
                    request_line: model::RequestLine {
                        http_version: WithDefault::default(),
                        method: WithDefault::Some(HttpMethod::POST),
                        target: model::RequestTarget::Absolute {
                            uri: "http://example.com/api/add".to_string()
                        }
                    },
                    settings: RequestSettings::default(),
                    pre_request_script: None,
                    response_handler: None,
                    save_response: None,
                },
                model::Request {
                    name: None,
                    comments: vec![],
                    headers: vec![],
                    body: model::RequestBody::None,
                    request_line: model::RequestLine {
                        http_version: WithDefault::default(),
                        method: WithDefault::Some(HttpMethod::GET),
                        target: model::RequestTarget::Absolute {
                            uri: "https://example.com/first".to_string()
                        }
                    },
                    settings: RequestSettings::default(),
                    pre_request_script: None,
                    response_handler: None,
                    save_response: None,
                },
                model::Request {
                    name: None,
                    comments: vec![],
                    headers: vec![],
                    body: model::RequestBody::None,
                    request_line: model::RequestLine {
                        http_version: WithDefault::default(),
                        method: WithDefault::Some(HttpMethod::GET),
                        target: model::RequestTarget::Absolute {
                            uri: "https://example.com/second".to_string()
                        }
                    },
                    settings: RequestSettings::default(),
                    pre_request_script: None,
                    response_handler: None,
                    save_response: None
                }
            ],
        );
}

Serialization Model -> Text

Either serialize to a file given a HttpRestFile model: http_rest_file::Serializer::serialize_to_file(rest_file_model)

Or serialize to a string given a list of request models, each will be serialized and requests are separated by the request separator which are three tags: ###

http_rest_file::Serializer::serialize_requests(requests)

Full example:

    #[test]
    pub fn serialize_with_headers() {
        let request = Request {
            name: None,
            headers: vec![Header::new("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36")
, Header::new("Accept-Language", "en-US,en;q=0.9,es;q=0.8"),
                // fake token
                Header::new("Authorization", "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"),
Header::new("Cache-Control", "max-age=3600")
            ],
            comments: vec![],
            settings: RequestSettings {
                no_redirect: None,
                no_log: None,
                no_cookie_jar: None,
            },
            request_line: RequestLine {
                method: WithDefault::Some(HttpMethod::POST),
                target: RequestTarget::from("https://httpbin.org/post"),
                http_version: WithDefault::default(),
            },
            body: RequestBody::None,
            pre_request_script: None,
            response_handler: None,
            save_response: None,
        };
        // we expect a newline after the headers
        let expected = r"POST https://httpbin.org/post
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/93.0.4577.63 Safari/537.36
Accept-Language: en-US,en;q=0.9,es;q=0.8
Authorization: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Cache-Control: max-age=3600
";
        let serialized = Serializer::serialize_requests(&[&request]);
        assert_eq!(serialized, expected);
    }

Error Handling And Recovery

During parsing errors can occur. See the ParseError enum type for a display of the kind of errors that can occur and what kind of messages are displayed if they do.

If a request can be parsed partially then the ErrorWithPartial will be returned which contains a PartialRequest with all parts that could be parsed before the error occurred. From a PartialRequest you can if you want create a Request model where all successfully parsed parts are present and the rest is filled up with the defaults.