起因

前几天因为某些需要,使用了PHP的一个requests库,Github:https://github.com/rmccue/Requests,说是Requests for PHP is a humble HTTP request library
用了一段时间感觉挺不错的,缺点就是文档,很不清晰,例如get方法的options参数,只点明了是个数组,但是具体有哪些可配置项并没有明确的说明。
无意间发现了一个bug,简单的讲,就是在get一个302的网页时,如果出现Location: ../test.php这种形式,requests库会抛出一个异常:

Fatal error: Uncaught exception 'Requests_Exception' with message 'Only HTTP requests are handled.' in D:\wamp64\www\test\vendor\rmccue\requests\library\Requests.php on line 480

这就十分尴尬了,所以决定自己修一下。

修bug

先搞个测试环境,写几个php文件复现一下:

redirect.php

<?php

header("Location: ../test.php");
echo 111111;

test.php随便写点什么,输出个hello world之类的。

fixRequests.php

<?php

require_once "vendor/autoload.php";
Requests::register_autoloader();

$r = Requests::get("http://127.0.0.1/test/redirect.php");
# $r = Requests::get("http://127.0.0.1/test.php");
var_dump($r->body);

访问http://127.0.0.1/test/fixRequests.php,果然抛出了异常。我们追踪一下get函数。
在/vendor/rmccue/requests/library/Request.php的183行。

/**#@+
* @see request()
* @param string $url
* @param array $headers
* @param array $options
* @return Requests_Response
*/
/**
* Send a GET request
*/
public static function get($url, $headers = array(), $options = array()) {
    return self::request($url, $headers, null, self::GET, $options);
}

继续追踪request函数,在297行。

public static function request($url, $headers = array(), $data = array(), $type = self::GET, $options = array()) {
    if (empty($options['type'])) {
        $options['type'] = $type;
    }
    $options = array_merge(self::get_default_options(), $options);

    self::set_defaults($url, $headers, $data, $type, $options);

    $options['hooks']->dispatch('requests.before_request', array(&$url, &$headers, &$data, &$type, &$options));

    if (!empty($options['transport'])) {
        $transport = $options['transport'];

        if (is_string($options['transport'])) {
            $transport = new $transport();
        }
    }
    else {
        $transport = self::get_transport();
    }
    $response = $transport->request($url, $headers, $data, $options);

    $options['hooks']->dispatch('requests.before_parse', array(&$response, $url, $headers, $data, $type, $options));

    return self::parse_response($response, $url, $headers, $data, $options);
}

可以看到在317行调用$transport->request获取了结果,之后调用parse_response解析结果。
猜测是在这里解析的response header,发现有302状态码,然后去进行跳转相关的操作。所以继续跟进。

protected static function parse_response($headers, $url, $req_headers, $req_data, $options) {
    
    // 省略无关代码。。。
    
    if ((in_array($return->status_code, array(300, 301, 302, 303, 307)) || $return->status_code > 307 && $return->status_code < 400) && $options['follow_redirects'] === true) {
        if (isset($return->headers['location']) && $options['redirected'] < $options['redirects']) {
            if ($return->status_code === 303) {
                $options['type'] = Requests::GET;
            }
            $options['redirected']++;
            $location = $return->headers['location'];
            if (strpos ($location, '/') === 0 ) {
                // relative redirect, for compatibility make it absolute
                $location = Requests_IRI::absolutize($url, $location);
                $location = $location->uri;
            }
            $redirected = self::request($location, $req_headers, $req_data, false, $options);
            $redirected->history[] = $return;
            return $redirected;
        }
        elseif ($options['redirected'] >= $options['redirects']) {
            throw new Requests_Exception('Too many redirects', 'toomanyredirects', $return);
        }
    }

    $return->redirects = $options['redirected'];

    $options['hooks']->dispatch('requests.after_request', array(&$return, $req_headers, $req_data, $options));
    return $return;
}

可以看到就在这部分进行的跳转处理,发现关键的部分:

if (strpos ($location, '/') === 0 ) {
    // relative redirect, for compatibility make it absolute
    $location = Requests_IRI::absolutize($url, $location);
    $location = $location->uri;
}

这里把Location格式化成了绝对URL的形式,但是作者只考虑到了以/开头的形式,没考虑到../这种。
所以我们自己修改一下就好了。

if (strpos ($location, '/') === 0 || strpos($location, '../') ) {
    // relative redirect, for compatibility make it absolute
    $location = Requests_IRI::absolutize($url, $location);
    $location = $location->uri;
}

再测试一下302跳转,发现已经可以正常使用了。

顺便发现了两个参数

看代码的过程中发现了options的两个参数,一个是follow_redirects,另一个是redirects
根据代码含义可以看出,follow_redirects是决定是否跟随302跳转的,而redirects是决定最大跳转次数的。
作者考虑的挺周到的,但是为什么文档那么差啊!(╯‵□′)╯︵┻━┻