Skip to content
This repository

Proposal for a HTTP Client interface #24

Open
wants to merge 4 commits into from

18 participants

Benjamin Eberlei AmyStephen Kris Wallsmith Lukas Kahwe Smith Michael Dowling Igor Wiedler Dominik Zogg Nicholas Humfrey Paul Dragoonis Maksim Kotlyar Phil Sturgeon Ryan McCue Chuck Reeves Marlin Cremers Matt Farina Crell Alexander Makarov
Benjamin Eberlei

From the introduction:

Many libraries and applications require an HTTP client to talk to other servers. In general all these libraries either ship their own HTTP client library, or use low-level PHP functionality such as file_get_contents, ext/curl or the ext/socket. Depending on the use-cases there are very tricky implementation details to be handled such as proxies, SSL, authentication protocols and many more. Not every small library can afford to implement all the different details. However there are only a few http client libraries that are widely used between different projects, because of NIH or fears of vendor lock-in.

Motivation:

Doctrine has about 4 projects that need an HTTP client. Currently every projects implements them itself, which is annoying. We could abstract this into our "Common" library, however that would mean we would start being a "http client" vendor. Now personally, i dont care about http clients and would rather let others do this, however i also don't want to face vendor lock-in by deciding for any of the many http clients.

proposed/http-client.md
((54 lines not shown))
  54 + *
  55 + * @return string
  56 + */
  57 + public function getContentType();
  58 +
  59 + /**
  60 + * Get the content of this response
  61 + *
  62 + * @return string
  63 + */
  64 + public function getContent();
  65 +
  66 + /**
  67 + * Return all headers of this response.
  68 + *
  69 + * @return array
5

What sort of array?

array('Content-Type: text/html');
array('Content-Type' => 'text/html');
array('Content-Type' => array('text/html'));
Benjamin Eberlei
beberlei added a note

Good question, I am open for suggestions ;-)

I would say header names as keys for sure. The third one is the most correct one i guess.

Case-insensitivity of keys is also an important consideration if we use header names as keys. Set-Cookie: asdf and set-cookie: asdfasdf should go under the same key.

Michael Dowling
mtdowling added a note

Always returning an array for every header seems clunky to me. Why not just return an array or string depending on if multiple headers of the same name were encountered?

<?php
var_export($response->getHeaders());
array(
  'Content-Type' => 'text/html',
  'Set-Cookie' => array('foo', 'bar')
);

The same could apply for retrieving specific headers:
$response->getHeader('Content-Type'); // "text/html"
$response->getHeader('Set-Cookie'); // array('foo', 'bar')

Matt Farina
mattfarina added a note

While I like the idea of getting a single header retrieved as a string, shouldn't the response from this call be consistent?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
AmyStephen

Any reason not to use https://github.com/symfony/symfony/tree/master/src/Symfony/Component/HttpFoundation as a standard? Symfony is already an 'http client' vendor and it's available now as a separate component. Drupal recently adopted it. I'm using it within Molajo. Excellent code, IMO.

Kris Wallsmith

HttpFoundation is designed for processing requests, not sending requests.

Lukas Kahwe Smith

@AmyStephen HttpFoundation could be a basis to fill in the proposed parameter for Request and proposed returned Response, but it does not include an interface for an HTTP client

AmyStephen

Missed that, sorry. Thanks guys.

Michael Dowling mtdowling commented on the diff
proposed/http-client.md
((17 lines not shown))
  17 + * A HTTP Client
  18 + */
  19 + interface HttpClient
  20 + {
  21 + /**
  22 + * Send a http-request and return a http-response.
  23 + *
  24 + * @param string $method HTTP method, uppercase
  25 + * @param string $url Url to send HTTP request to
  26 + * @param string $content Content of the request, can be empty.
  27 + * @param array $headers Array of Headers, header Name is the key.
  28 + * @param array $options Vendor specific options to activate specific features.
  29 + * @throws HttpException If no response can be created an exception should be thrown.
  30 + * @return Response
  31 + */
  32 + public function request($method, $url, $content = null, array $headers = array(), array $options = array());
7
Michael Dowling
mtdowling added a note

I'd like to see the method signature become:

public function request($method, $url, array $headers = array(), $content = null, array $options = array());

I find that I almost always have to set headers (e.g. Accept, Content-Type) when sending requests to REST APIs. I think that moving $headers before $content would help to make it easier to send GET/HEAD/DELETE requests. Doing it this way, Guzzle could just add a new request method to its client and implement the HttpClient interface.

Phil Sturgeon Collaborator
philsturgeon added a note

Agreed, I set headers on a request more often than setting content.

Ryan McCue
rmccue added a note

+1 on headers earlier. Requests uses $url, $headers, $data, $type, $options, which is based on usage statistics. (It also means you can just do Requests::request('http://example.com/') and forget the rest, with GET implied.)

Matt Farina
mattfarina added a note

Two thoughts,

  1. Drupal (before it was arrayified) had $headers before content (I use headers more than content) and $method (aka type) later as well with GET being assumed as a default. I think getting a sane order is important to ease the pain of a lot of use. drupal_http_request is the Drupal version.
  2. An idea would be to make this chainable method calls. Instead of numerous arguments on one method having one method per argument. Spitballing here:
$foo = new Client();
$foo->method('GET')->url('http://foo.com')->execute();
Crell
Crell added a note

I have to agree with Matt's note 2 here. Sending an HTTP request is a Command. It should be a Command object, not a method. Vis, you create a Request/Client/Whatever object, configure it with whatever methods in whatever order you want, then execute()/send()/whatever(). And get back a Response. That gives maximum flexibility to users, as well as allows for specific implementers to add additional methods if needs be without breaking the interface.

Alexander Makarov
samdark added a note

+1 for @mattfarina idea number 2 and @Crell corrections about request object.

Phil Sturgeon Collaborator

Fan of that (number 2)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Michael Dowling mtdowling commented on the diff
proposed/http-client.md
((11 lines not shown))
  11 +A single interface is proposed
  12 +
  13 + <?php
  14 + namespace PSR\Http\Client;
  15 +
  16 + /**
  17 + * A HTTP Client
  18 + */
  19 + interface HttpClient
  20 + {
  21 + /**
  22 + * Send a http-request and return a http-response.
  23 + *
  24 + * @param string $method HTTP method, uppercase
  25 + * @param string $url Url to send HTTP request to
  26 + * @param string $content Content of the request, can be empty.
2
Michael Dowling
mtdowling added a note

Should $content accept a string OR array for application/x-www-form-urlencoded POST requests?

Alexander Makarov
samdark added a note

There's no easy way to send files as multipart if $content is just string.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Igor Wiedler
igorw commented

This might not be useful to so many people, but since it's really easy to add, having an async interface would be nice too.

I'm thinking:

interface ResponsePromise
{
    public function onResponse(callable $callback);
    public function onError(callable $callback);
}

interface AsyncHttpClient
{
    public function request($method, $url, $content = null, array $headers = array(), array $options = array());
}

Usage:

$client = new AsyncHttpClient();
$request = $client->request('GET', 'http://example.com/');
$request->onResponse(function (Response $response) {
    var_dump($response->getContent());
});
$request->onError(function (HttpException $e) {
    var_dump($e->getMessage());
});

Thoughts?

Benjamin Eberlei
Igor Wiedler
igorw commented

I suppose forcing the client to buffer the request is suboptimal. A method on the promise to explicitly start the request would be an option, however I don't know if there are any conventions for this.

Phil Sturgeon philsturgeon commented on the diff
proposed/http-client.md
((42 lines not shown))
  42 + */
  43 + class Response
  44 + {
  45 + /**
  46 + * Status code for the response
  47 + *
  48 + * @return int
  49 + */
  50 + public function getStatusCode();
  51 +
  52 + /**
  53 + * Get content type for the response
  54 + *
  55 + * @return string
  56 + */
  57 + public function getContentType();
7
Phil Sturgeon Collaborator
philsturgeon added a note

If we start adding helper functions for headers like this, would we not need to start adding a whole lot more? Expiry dates, cache settings, language, etc?

Marlin Cremers
Marlinc added a note

Why not create a Headers object? So you could get/set headers in it and pass that to the request
And so you could use that to get the header info instead of all those methods in the response class

Alexander Makarov
samdark added a note

@Marlinc isn't it too many objects for a well-defined (by RFC) topic?

Phil Sturgeon Collaborator

I figure getHeader('Content-Type') would make the most sense. No point adding methods for the sake of it.

Alexander Makarov
samdark added a note

Why not? It's one of the most used ones. We're getting less typo prone way of doing it + IDE code autocompletion.

Phil Sturgeon Collaborator
Alexander Makarov
samdark added a note

OK. Makes sense from this point.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Dominik Zogg

Are there any news about this, working on a lib, i could need this?

Nicholas Humfrey
njh commented

Is this ever likely to be approved?

Nicholas Humfrey njh referenced this pull request in njh/easyrdf
Open

HTTP client change #133

Paul Dragoonis
Owner

@njh it's still in progress

Maksim Kotlyar

@beberlei what should be done to move it forward? Is it really possible to have this PR be approved? Could I help somehow?

Dominik Zogg

https://github.com/payment/httpclient
i needed one for by saferpay library

Phil Sturgeon
Collaborator

I think a problem here is that this is too alien to the existing HTTP clients to really be a useful standard.

http://guzzlephp.org/tour/http.html
https://github.com/kriswallsmith/Buzz
https://github.com/rmccue/Requests

All of these systems have different methods for get(), posts(), put(), delete() for example, because they all work differently.

Delete does not have a body, so no need for $content on that one. get() needs to turn $content into a query string, and the rest have to either detect an array and set a default mime type, or whatever.

This is only an interface of course, but every implementation will need to have these different methods to do much, so I feel like adding them to the interface would be much more useful. I want to know that there is a get() method, not just hope there is one in the specific implementation.

I could be wrong :)

Phil Sturgeon
Collaborator

The whole point of the HTTP PSR is so you don't need bridge packages or adapter classes, so that's not a great idea.

Dominik Zogg

@philsturgeon i don't think, then ever will be an http psr

Phil Sturgeon
Collaborator

image

Dominik Zogg

@philsturgeon but i think its much better writing bridges against an interface, than, have nothing (cause i don't want to force others using, buzz, or guzzle or whatever)

Ryan McCue
rmccue commented

Personally, I'm not going to change Requests to a PSR interface (because a. backwards compatibility, and b. extra dependencies), but I'm definitely going to offer a bridge if/when the PSR is stable.

Phil Sturgeon
Collaborator
Phil Sturgeon
Collaborator
Ryan McCue
rmccue commented

Right, you can offer a separate class to expose a PSR\HTTP compatible API without recoding your whole package.

Exactly. I meant the previous more as a comment that instead of users writing bridges, the library authors can do so and users can rely on a common interface.

Phil Sturgeon
Collaborator
Alexander Makarov samdark commented on the diff
proposed/http-client.md
((12 lines not shown))
  12 +
  13 + <?php
  14 + namespace PSR\Http\Client;
  15 +
  16 + /**
  17 + * A HTTP Client
  18 + */
  19 + interface HttpClient
  20 + {
  21 + /**
  22 + * Send a http-request and return a http-response.
  23 + *
  24 + * @param string $method HTTP method, uppercase
  25 + * @param string $url Url to send HTTP request to
  26 + * @param string $content Content of the request, can be empty.
  27 + * @param array $headers Array of Headers, header Name is the key.
1
Alexander Makarov
samdark added a note

RFC2616:

Multiple message-header fields with the same field-name MAY be present in a message if and only if the entire field-value for that header field is defined as a comma-separated list [i.e., #(values)]. It MUST be possible to combine the multiple header fields into one "field-name: field-value" pair, without changing the semantics of the message, by appending each subsequent field-value to the first, each separated by a comma. The order in which header fields with the same field-name are received is therefore significant to the interpretation of the combined field value, and thus a proxy MUST NOT change the order of these field values when a message is forwarded.

Cache-Control: no-cache
Cache-Control: no-store

Is valid.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Alexander Makarov samdark commented on the diff
proposed/http-client.md
((35 lines not shown))
  35 +A value objects come with this interface, the Response which is returned from the client. Their definition is as follows:
  36 +
  37 + <?php
  38 + namespace PSR\Http\Client;
  39 +
  40 + /**
  41 + * Http Response returned from {@see HttpClient::request}.
  42 + */
  43 + class Response
  44 + {
  45 + /**
  46 + * Status code for the response
  47 + *
  48 + * @return int
  49 + */
  50 + public function getStatusCode();
1
Alexander Makarov
samdark added a note

Status message is useful for exceptions so probably worth adding as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Alexander Makarov samdark commented on the diff
proposed/http-client.md
((61 lines not shown))
  61 + *
  62 + * @return string
  63 + */
  64 + public function getContent();
  65 +
  66 + /**
  67 + * Return all headers of this response.
  68 + *
  69 + * Header names are returned as lower-case keys. Their values are returned
  70 + * in an array themselves to support multiple occurances of headers.
  71 + *
  72 + * @example array(array("content-type" => array("text/html"))
  73 + *
  74 + * @return array
  75 + */
  76 + public function getHeaders();
20
Alexander Makarov
samdark added a note

Same as above. Multiple same-named headers are possible.

What's wrong with this? Key is a string and value is an array, so we can have multiple values for one key like

$headers = [
    'Cache-Control' => [
        'no-cache',
        'no-store'
    ]
];
Alexander Makarov
samdark added a note

Ah, then it's totally fine. One question is should it be always an array or sometimes it will be a scalar?

Alexander Makarov
samdark added a note

Also it cannot be forced programmaticaly via interface if made this way so there's a verbal agreement (not that bad but still).

Phil Sturgeon Collaborator

We don't need to programmatically enforce header names, as they can be anything.

You can have multiple headers of the same name and they override each other, that should be your responsibility to sort them out as a developer though, right?

Alexander Makarov
samdark added a note

They should not override each other as stated in RFC2616.

Phil Sturgeon Collaborator

It depends where you are talking about the override happening. If you are creating a $headers array to pass to setHeaders() then you as a developer are responsible for working that out properly, right?

If its setHeader('Cache-Control') then that is a completely different story. But RFC2616 has no relevance to the creation of a PHP array in the developers own domain.

Alexander Makarov
samdark added a note

Yes. getHeaders should return something like:

[
  header name -> header value,
  header name -> [header value1, header value2],
]

It's not only Cache-Control that can have multiple values. For example:

WWW-Authenticate: Negotiate 
WWW-Authenticate: Basic realm="EXAMPLE.COM" 

So setting headers should support arrays as well.

Phil Sturgeon Collaborator

Well it's not at all possible for PHP to return that structure.

[
  header name => header value,
  header name => [header value1, header value2],
]

Keys with matching names aren't possible.

It could maybe return:

[
  header name => [
    header value,
    [header value1, header value2],
  ]
]

That structure could maybe go into setHeaders, but it's getting a bit disgusting.

Alexander Makarov
samdark added a note

I meant

[
  header name1 -> header value1,
  header name2 -> [header value2, header value3],
]

sorry for confusion.

Phil Sturgeon Collaborator

I actually think header values should be managed in an object. Here's what I'm thinking (it's an almost rewritten fork of this PR): https://github.com/mtdowling/fig-standards/blob/modified-http/proposed/http.md#32-psrhttpheadervaluesinterface

Alexander Makarov
samdark added a note

Umm, it's just extends \Countable, \Traversable, \ArrayAccess. Isn't it just the same as array (except consuming more memory and being an object)? What are pros?

Phil Sturgeon Collaborator

That looks boss. You should factor this feedback in (above):

An idea would be to make this chainable method calls. Instead of numerous arguments on one method having one method per argument. Spitballing here:

$foo = new Client();
$foo->method('GET')->url('http://foo.com')->execute();
Phil Sturgeon Collaborator
Alexander Makarov
samdark added a note
  1. It doesn't have anything to do with chaining. Chaining is just request methods returning the instance of request that collects data including headers. I see no pro for these headers to be objects if these objects do not define any structure.
  2. Type hinting doesn't make sense for an object that accepts any key-value pairs. It is a wrapper for the purpose of having a wrapper.

Being able to treat the values of a header as both an array and a string are the best of both worlds:

echo $response->getHeader('Cache-Control');
//> no-cache, no-store

echo $response->getHeader('Cache-Control')[0];
//> no-cache

echo count($response->getHeader('Cache-Control'));
//> 2

foreach ($response->getHeader('Cache-Control') as $index => $value) {
    echo $value . ', ';
}
//> no-cache, no-store
Alexander Makarov
samdark added a note

@mtdowling looks a bit too magical to me.

Phil Sturgeon Collaborator

@samdark I'm sorry to have caused confusion.

It doesn't have anything to do with chaining. Chaining is just request methods returning the instance of request that collects data including headers.

Right, this specific conversation has nothing to do with chaining. I was suggesting that should go into @mtdowling's PR, which is a fork of this.

Type hinting doesn't make sense for an object that accepts any key-value pairs. It is a wrapper for the purpose of having a wrapper.

Instead of a method expecting array it can type hint against HeaderValuesInterface, which is marginally more useful in some scenarios. I wasn't trying to suggest that we need type hinting on the key/value pair strings or anything weird like that.

@mtdowling thats some great stuff right there.

Ryan McCue
rmccue added a note

IMO, easier to move the logic up the stack into the header dictionary. Requests uses a case-insensitive dictionary which implements ArrayAccess, giving:

// X-Multiple: first-value
// x-MULTIPLE: second-value
$value = $headers['x-multiple'];
// 'first-value,second-value'
$values = $headers->getValues('x-multiple');
// array( 'first-value', 'second-value' )

Originally, it didn't have a distinction here and always used the first form, however thanks to the wonderful world of HTTP specifications, that doesn't always work. Notably, cookie headers will cause problems if you always concatenate with a comma, as commas are used in the old specification in the Expires part.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Chuck Reeves

Should we also include some method to tell the class to set how many redirects to follow as well?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.

Showing 1 changed file with 114 additions and 0 deletions. Show diff stats Hide diff stats

  1. +114 0 proposed/http-client.md
114 proposed/http-client.md
Source Rendered
... ... @@ -0,0 +1,114 @@
  1 +## Introduction
  2 +
  3 +Many libraries and applications require an HTTP client to talk to other servers. In general all these libraries either ship their own HTTP client library, or use low-level PHP functionality such as `file_get_contents`, ext/curl or the ext/socket. Depending on the use-cases there are very tricky implementation details to be handled such as proxies, SSL, authentication protocols and many more. Not every small library can afford to implement all the different details. However there are only a few http client libraries that are widely used between different projects, because of NIH or fears of vendor lock-in.
  4 +
  5 +## Goal
  6 +
  7 +This proposal aims at providing a very simple HTTP client interface to allow interopability between HTTP clients of different libraries and allow PHP libraries to ship HTTP client functionality without the necessity to implement a client.
  8 +
  9 +## Interfaces
  10 +
  11 +A single interface is proposed
  12 +
  13 + <?php
  14 + namespace PSR\Http\Client;
  15 +
  16 + /**
  17 + * A HTTP Client
  18 + */
  19 + interface HttpClient
  20 + {
  21 + /**
  22 + * Send a http-request and return a http-response.
  23 + *
  24 + * @param string $method HTTP method, uppercase
  25 + * @param string $url Url to send HTTP request to
  26 + * @param string $content Content of the request, can be empty.
  27 + * @param array $headers Array of Headers, header Name is the key.
  28 + * @param array $options Vendor specific options to activate specific features.
  29 + * @throws HttpException If no response can be created an exception should be thrown.
  30 + * @return Response
  31 + */
  32 + public function request($method, $url, $content = null, array $headers = array(), array $options = array());
  33 + }
  34 +
  35 +A value objects come with this interface, the Response which is returned from the client. Their definition is as follows:
  36 +
  37 + <?php
  38 + namespace PSR\Http\Client;
  39 +
  40 + /**
  41 + * Http Response returned from {@see HttpClient::request}.
  42 + */
  43 + class Response
  44 + {
  45 + /**
  46 + * Status code for the response
  47 + *
  48 + * @return int
  49 + */
  50 + public function getStatusCode();
  51 +
  52 + /**
  53 + * Get content type for the response
  54 + *
  55 + * @return string
  56 + */
  57 + public function getContentType();
  58 +
  59 + /**
  60 + * Get the content of this response
  61 + *
  62 + * @return string
  63 + */
  64 + public function getContent();
  65 +
  66 + /**
  67 + * Return all headers of this response.
  68 + *
  69 + * Header names are returned as lower-case keys. Their values are returned
  70 + * in an array themselves to support multiple occurances of headers.
  71 + *
  72 + * @example array(array("content-type" => array("text/html"))
  73 + *
  74 + * @return array
  75 + */
  76 + public function getHeaders();
  77 +
  78 + /**
  79 + * Return a specified header of this response or null if not found,
  80 + *
  81 + * Returns an array no matter if single or multiple occurances of a header exist.
  82 + *
  83 + * @return array|null
  84 + */
  85 + public function getHeader($name);
  86 + }
  87 +
  88 +Also an exception is shipped:
  89 +
  90 + <?php
  91 + namespace PSR\Http\Client;
  92 +
  93 + class HttpException extends \RuntimeException
  94 + {
  95 + }
  96 +
  97 +## Sample code
  98 +
  99 +Here is an example usage:
  100 +
  101 + <?php
  102 + $client = create_http_client(); // implementation specific
  103 + $response = $client->request('GET', 'http://www.php.net');
  104 +
  105 + if ($response->getStatusCode() == 200) {
  106 + $content = $response->getContent();
  107 + }
  108 +
  109 + $response = $client->request('GET', 'http://api/returning.json');
  110 +
  111 + if ($response->getContentType() == 'application/json') {
  112 + $json = json_decode($response->getContent());
  113 + }
  114 +

Tip: You can add notes to lines in a file. Hover to the left of a line to make a note

Something went wrong with that request. Please try again.