NotionCommotion Posted December 21, 2018 Share Posted December 21, 2018 Using Slim to route endpoints to my application. In addition, I have many endpoints (mostly accessed via xhr) which need to be forwarded to another server, and I am using Guzzle to do so. Note only do I have to transfer text/json, I also have to send and retrieve files (currently only csv files, but will later add pdf). Accomplishing this was easier than I expected, but expect I may still be doing certain portions wrong. Anything look off, especially with the multipart forms for file uploads as well as the downloading of files? Thank you <?php $app = new \Slim\App($container); //Local requests $app->get('/settings', function (Request $request, Response $response) { return $this->view->render($response, 'somePage.html',$this->bla->getData()); }); //more local endpoints... $proxyEndpoints=[ '/bla'=>['put'], '/bla/bla/{id:[0-9]+}'=>['delete','put'], '/foo/{id:[0-9]+}'=>['get','put','delete','post'], //more proxy endpoints... ]; foreach ($proxyEndpoints as $route=>$methods) { foreach ($methods as $method) { $app->$method($route, function(Request $request, Response $response) { return $this->remoteServer->proxy($request, $response); //add content type if desired. }); } } <?php class RemoteServer { protected $httpClient, $contentType; public function __construct(\GuzzleHttp\Client $httpClient, string $contentType='application/json') { $this->httpClient=$httpClient; $this->contentType=$contentType; } public function proxy(\Slim\Http\Request $request, \Slim\Http\Response $response, string $contentType=null, \Closure $callback=null):\Slim\Http\Response { $contentType=$contentType??$this->contentType; if($contentType!=='application/json' && $callback) { throw new \Exception('Callback can only be used with contentType application/json'); } $method=$request->getMethod(); $bodyParams=in_array($method,['PUT','POST'])?(array)$request->getParsedBody():[]; //Ignore body for GET and DELETE methods $queryParams=$request->getQueryParams(); $data=array_merge($queryParams, $bodyParams); ///Would be better to write slim's body to guzzle's body so that get parameters are preserved and not overriden by body parameters. $path=$request->getUri()->getPath(); $contentTypeHeader=$request->getContentType(); if(substr($contentTypeHeader, 0, 19)==='multipart/form-data'){ syslog(LOG_INFO, 'contentType: '.$contentTypeHeader); $files = $request->getUploadedFiles(); $multiparts=[]; $errors=[]; foreach($files as $name=>$file) { if ($error=$file->getError()) { $errors[]=[ 'name'=> $name, 'filename'=> $file->getClientFilename(), 'error' => $this->getFileErrorMessage($error) ]; } else { $multiparts[]=[ 'name'=> $name, 'filename'=> $file->getClientFilename(), 'contents' => $file->getStream(), 'headers' => [ //Not needed, right? 'Size' => $file->getSize(), 'Content-Type' => $file->getClientMediaType() ] ]; } } if($errors) return $response->withJson($errors, 422); $multiparts[]=[ 'name'=> 'data', 'contents' => json_encode($data), 'headers' => ['Content-Type' => 'application/json'] ]; $options=['multipart' => $multiparts]; } else { $options = in_array($method,['PUT','POST'])?['json'=>$data]:['query'=>$data]; } try { $curlResponse = $this->httpClient->request($method, $path, $options); } catch (\GuzzleHttp\Exception\RequestException $e) { //Errors only return JSON //Networking error which includes ConnectException and TooManyRedirectsException syslog(LOG_ERR, 'Proxy error: '.$e->getMessage()); if ($e->hasResponse()) { $curlResponse=$e->getResponse(); return $response->withJson(json_decode($curlResponse->getBody()), $curlResponse->getStatusCode()); } else { return $response->withJson($e->getMessage(), $e->getMessage()); } } $statusCode=$curlResponse->getStatusCode(); switch($contentType) { case 'application/json': //Application and server error messages will be returned. Consider hiding server errors. $content=json_decode($curlResponse->getBody()); if($callback) { $content=$callback($content, $statusCode); } return $response->withJson($content, $statusCode); case 'text/html': case 'text/plain': //Application and server error messages will be returned. Consider hiding server errors. $response = $response->withStatus($statusCode); return $response->getBody()->write($curlResponse->getBody()); case 'text/csv': foreach ($response->getHeaders() as $name => $values) { syslog(LOG_INFO, "headers: $name: ". implode(', ', $values)); } if($statusCode===200) { return $response->withHeader('Content-Type', 'application/force-download') ->withHeader('Content-Type', 'application/octet-stream') ->withHeader('Content-Type', 'application/download') ->withHeader('Content-Description', 'File Transfer') ->withHeader('Content-Transfer-Encoding', 'binary') ->withHeader('Content-Disposition', 'attachment; filename="data.csv"') ->withHeader('Expires', '0') ->withHeader('Cache-Control', 'must-revalidate, post-check=0, pre-check=0') ->withHeader('Pragma', 'public') ->withBody($curlResponse->getBody()); } else { return $response->withJson(json_decode($curlResponse->getBody()), $statusCode); } break; default: throw new \Exception("Invalid proxy contentType: $contentType"); } } private function getFileErrorMessage($code){ switch ($code) { case UPLOAD_ERR_INI_SIZE: $message = "The uploaded file exceeds the upload_max_filesize directive in php.ini"; break; case UPLOAD_ERR_FORM_SIZE: $message = "The uploaded file exceeds the MAX_FILE_SIZE directive that was specified in the HTML form"; break; case UPLOAD_ERR_PARTIAL: $message = "The uploaded file was only partially uploaded"; break; case UPLOAD_ERR_NO_FILE: $message = "No file was uploaded"; break; case UPLOAD_ERR_NO_TMP_DIR: $message = "Missing a temporary folder"; break; case UPLOAD_ERR_CANT_WRITE: $message = "Failed to write file to disk"; break; case UPLOAD_ERR_EXTENSION: $message = "File upload stopped by extension"; break; default: $message = "Unknown upload error"; break; } return $message; } public function callApi(\GuzzleHttp\Psr7\Request $request, array $data=[]):\GuzzleHttp\Psr7\Response { try { $response = $this->httpClient->send($request, $data); } catch (\GuzzleHttp\Exception\ClientException $e) { $response=$e->getResponse(); } catch (\GuzzleHttp\Exception\RequestException $e) { //Networking error which includes ConnectException and TooManyRedirectsException if ($e->hasResponse()) { $response=$e->getResponse(); } else { $response=new \GuzzleHttp\Psr7\Response($e->getCode(), [], $e->getMessage()); } } catch (\GuzzleHttp\Exception\ServerException $e) { //Consider not including all information back to client $response=$e->getResponse(); } return $response; } } Quote Link to comment Share on other sites More sharing options...
NotionCommotion Posted January 1, 2019 Author Share Posted January 1, 2019 Turns out this is even easier than I thought. My configuration has two http servers on the same machine where one is mainwebsite.com and it makes curl requests to the other which is api.mainwebsite.com . Now, I am just sending the Slim Request to Guzzle. Originally, I sent Slim's Request with its original host (mainwebsite.com), and my $_SESSION variable kept on getting deleted. I am assuming some sort of web browser or PHP security feature? Any thoughts? Why wasn't the base_uri added to \GuzzleHttp\Client's constructor not being applied? Regardless, I was able to change the host as shown below and all now works. To use less code, I am using a loop to apply the headers to the response. Think I should hard code it like $slimResponse->withHeader('header1', $guzzleResponse->getHeader('header1'))->addMoreHeaders()->thenAddStatusAndBody()? This (I think) will eliminate needing to clone all the responses, however, will will require me to have code for each Content-Type to determine which headers are returned. But maybe that is good and I shouldn't blacklist headers as I am doing but whitelist them? If this approach is acceptable, what other headers should I blacklist? Thanks public function proxy(\Slim\Http\Request $slimRequest, \Slim\Http\Response $slimResponse):\Slim\Http\Response { //Forwards Slim Request to another server and returns the updated Slim Response. $slimRequest=$slimRequest->withUri($slimRequest->getUri()->withHost($this->getHost(false))); //Change slim's host to API server! try { $httpClient = new \GuzzleHttp\Client(['base_uri' => "https://api.mainwebsite.com"]); //Will use injection, and just shown this way ease $guzzleResponse=$httpClient->send($slimRequest); $excludedHeaders=['Date', 'Server', 'X-Powered-By', 'Access-Control-Allow-Origin', 'Access-Control-Allow-Methods', 'Access-Control-Allow-Headers']; $headerArrays=array_diff_key($guzzleResponse->getHeaders(), array_flip($excludedHeaders)); foreach($headerArrays as $headerName=>$headers) { foreach($headers as $headerValue) { $slimResponse=$slimResponse->withHeader($headerName, $headerValue); } } return $slimResponse->withStatus($guzzleResponse->getStatusCode())->withBody($guzzleResponse->getBody()); } catch (\GuzzleHttp\Exception\RequestException $e) { if ($e->hasResponse()) { $guzzleResponse=$e->getResponse(); return $slimResponse->withStatus($guzzleResponse->getStatusCode())->withBody($guzzleResponse->getBody()); } else { return $slimResponse->withStatus(500)->write(json_encode(['message'=>'RequestException without response: '.$e->getMessage()])); } } } 1 Quote Link to comment Share on other sites More sharing options...
NotionCommotion Posted January 3, 2019 Author Share Posted January 3, 2019 Ran into some odd issues. Things were working, but then all of a sudden the api server couldn't parse the content. Turns out that Guzzle is no longer forwarding the Content-Type header in Slim's request, and I needed to re-apply the Content-Type header. I have no idea what changed to require this as it could be at the browser client, web server/api client, or api server. Any ideas? if((string) $slimRequest->getBody()) $slimRequest=$slimRequest->withHeader('Content-Type', $slimRequest->getContentType()); Quote Link to comment Share on other sites More sharing options...
Recommended Posts
Join the conversation
You can post now and register later. If you have an account, sign in now to post with your account.