Delphi: Using Google URL Shortener with IdHTTP - 400 Bad Request - delphi

I'm trying to access the URL Shortener ( http://goo.gl/ ) via its API from within Delphi.
However, the only result I get is: HTTP/1.0 400 Bad Request (reason: parseError)
Here is my code (on a form with a Button1, Memo1 and IdHTTP1 that has IdSSLIOHandlerSocketOpenSSL1 as its IOHandler. I got the necessary 32-bit OpenSSL DLLs from http://indy.fulgan.com/SSL/ and put them in the .exe's directory):
procedure TFrmMain.Button1Click(Sender: TObject);
var html, actionurl: String;
makeshort: TStringList;
begin
try
makeshort := TStringList.Create;
actionurl := 'https://www.googleapis.com/urlshortener/v1/url';
makeshort.Add('{"longUrl": "http://slashdot.org/stories"}');
IdHttp1.Request.ContentType := 'application/json';
//IdHTTP1.Request.ContentEncoding := 'UTF-8'; //Using this gives error 415
html := IdHTTP1.Post(actionurl, makeshort);
memo1.lines.add(idHTTP1.response.ResponseText);
except on e: EIdHTTPProtocolException do
begin
memo1.lines.add(idHTTP1.response.ResponseText);
memo1.lines.add(e.ErrorMessage);
end;
end;
memo1.Lines.add(html);
makeshort.Free;
end;
Update: I have left off my API key in this example (should usually work well without one for a few tries), but if you want to try it with your own, you can substitute the actionurl string with
'https://www.googleapis.com/urlshortener/v1/url?key=<yourapikey>';
The ParseError message leads me to believe that there might be something wrong with the encoding of the longurl when it gets posted but I wouldn't know what to change.
I've been fuzzing over this for quite a while now and I'm sure the mistake is right before my eyes - I'm just not seeing it right now.
Any help is therefore greatly appreciated!
Thanks!

As you discovered, the TStrings overloaded version of the TIdHTTP.Post() method is the wrong method to use. It sends an application/x-www-form-urlencoded formatted request, which is not appropriate for a JSON formatted request. You have to use the TStream overloaded version of the TIdHTTP.Post() method instead`, eg:
procedure TFrmMain.Button1Click(Sender: TObject);
var
html, actionurl: String;
makeshort: TMemoryStream;
begin
try
makeshort := TMemoryStream.Create;
try
actionurl := 'https://www.googleapis.com/urlshortener/v1/url';
WriteStringToStream(makeshort, '{"longUrl": "http://slashdot.org/stories"}', IndyUTF8Encoding);
makeshort.Position := 0;
IdHTTP1.Request.ContentType := 'application/json';
IdHTTP1.Request.Charset := 'utf-8';
html := IdHTTP1.Post(actionurl, makeshort);
finally
makeshort.Free;
end;
Memo1.Lines.Add(IdHTTP1.Response.ResponseText);
Memo1.Lines.Add(html);
except
on e: Exception do
begin
Memo1.Lines.Add(e.Message);
if e is EIdHTTPProtocolException then
Memo1.lines.Add(EIdHTTPProtocolException(e).ErrorMessage);
end;
end;
end;

From the URL shortener API docs:
Every request your application sends to the Google URL Shortener API
needs to identify your application to Google. There are two ways to
identify your application: using an OAuth 2.0 token (which also
authorizes the request) and/or using the application's API key.
Your example does not contain code for OAuth or API key authentication.
To authenticate with an API key, the docs are clear:
After you have an API key, your application can append the query
parameter key=yourAPIKey to all request URLs.

Related

Load a png image from the web (delphi fmx)

I would like to load png images from the web, but with the code below not all images do get correctly into the stream (while posting the url in a webbrowser, e.g. edge, does give the image correctly). For example, the first url in the code gives a loading bitmap failed error (in the stream FSize is only 14?), while the second url does not give a problem. Does anyone know how to download the first url correctly?
For this piece of code to work, a TButton and a TImage was put on a form.
System.Net.HttpClientComponent was added in the uses. I am using Delphi 10.3.3. fmx.
Thanks,
Gerard
procedure TForm1.Button1Click(Sender: TObject);
var ms: TmemoryStream;
httpCli: TNetHTTPClient;
url: string;
begin
httpCli := TNetHTTPClient.Create(nil);
ms := TMemoryStream.Create();
url := 'https://a.tile.openstreetmap.org/11/1050/674.png';
// url := 'https://upload.wikimedia.org/wikipedia/commons/d/d5/Japan_small_icon.png';
httpCli.Get(url,ms);
ms.Position := 0;
Image1.Bitmap.LoadFromStream(ms);
ms.free;
httpCli.free;
end;
The problem with the OpenStreetMap tile server is the UserAgent. You must change the default value to something acceptable by the server. I checked a number of possibilities and it looks like almost anything but default value of TNetHTTPClient works. See this Wikipedia article for details.
To do that, you need to add the line
httpCli.UserAgent := 'Delphi/4.0 (compatible; Delphi; HttpClient)';
My version of your code which includes HTTP status code checking is the following:
procedure TForm1.Button1Click(Sender: TObject);
var
ms : TMemoryStream;
httpCli : TNetHTTPClient;
resp : IHTTPResponse;
url : String;
begin
httpCli := TNetHTTPClient.Create(nil);
try
httpCli.UserAgent := 'Delphi/4.0 (compatible; Delphi; HttpClient)';
ms := TMemoryStream.Create();
try
url := 'https://a.tile.openstreetmap.org/11/1050/674.png';
resp := httpCli.Get(url, ms);
if resp.StatusCode <> 200 then
Memo1.Lines.Add(Format('HTTP Error=%d %s',
[resp.StatusCode, resp.StatusText]))
else begin
ms.Position := 0;
Image1.Bitmap.LoadFromStream(ms);
end;
finally
ms.Free;
end;
finally
httpCli.Free;
end;
end;
When operating on HTTP you have to check if the HTTP server can satisfy your request (status code 200, as per RFC7231, § 6.3.1) or any error occured. In your case requesting that URI and making sure to see what comes from the server can be done with i.e. wget:
wget -S --content-on-error https://a.tile.openstreetmap.org/11/1050/674.png
This will print the server's response headers and in any case create a file that will hold the payload. The response headers are (excerpt):
HTTP/1.1 403 Forbidden
Content-Length: 14
Which means: you are not allowed to query that resource (HTTP status code 403). The payload is saved in a file sizing 14 bytes, containing this text in 14 ASCII characters:
Access denied.
Trivia: the smallest valid PNG file sizes at least 67 bytes (read "Smallest possible transparent PNG" by Gareth Rees.
This is a variation of fpiete's answer. I found this way simpler, with no need to create the TMemoryStream object, for example.
You can use the RESTRequest4D library for the request. The usage is very simple and I find it more intuitive than the regular HTTP client.
procedure TQRCodeForm.LoadImageFromURL;
var
LResponse: IResponse;
begin
LResponse := TRequest
.New
.BaseURL(ImageURL)
.Get;
if LResponse.StatusCode = 200 then
begin
MyTImage.Bitmap.LoadFromStream(LResponse.ContentStream);
end;
end;

Indy10 Google Securetoken always 403

I am struggling to get a response from Google Securetoken using Delphi 10.2.2 and Indy 10.
I've gotten my RefreshToken before from https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=XXX. This worked fine.
Now I try to refresh the token with https://securetoken.googleapis.com/v1/token, which doesn't work. I always get a 403 Forbidden error.
procedure TForm1.Button2Click(Sender: TObject);
var
l_Response: string;
l_PostData: TIdMultiPartFormDataStream;
l_IRESAccessToken: TIRESAccessToken;
begin
IdHTTP2.Request.ContentType := 'application/json';
IdHTTP2.Request.CharSet := 'utf-8';
l_PostData := TIdMultiPartFormDataStream.Create;
try
l_PostData.AddFormField('grant_type', 'refresh_token');
l_PostData.AddFormField('refresh_token', m_IRESAuth.RefreshToken);
l_PostData.AddFormField('key', 'XXX');
try
l_Response := IdHTTP2.Post('https://securetoken.googleapis.com/v1/token', l_PostData);
l_IRESAccessToken := TJson.JsonToObject<TIRESAccessToken>(l_Response);
except
on E: Exception do
ShowMessage('Error on request: '#13#10 + e.Message);
end;
finally
l_PostData.Free;
end;
end;
I tried an IOHandler that specifies TLS 1.2, and tried to send the post with a JSON object. Also, I set the hoKeepOrigProtocol flag in the TIdHTTP.HTTPOptions. Nothing worked so far, I always get 403 Forbidden.
I tried it with another program, no problems there.
Am I missing something?
Per Google's documentation:
Token Service REST API Reference:
HTTP request
POST https://securetoken.googleapis.com/v1/token
Request body
The request body contains data with the following structure:
URL-encoded representation
grant_type=string&code=string&refresh_token=string
The TStrings overload of TIdHTTP.Post() sends data in that format. That is the overload you need to use, not the TIdMultipartFormDataStream overload.
Also, as you can see above, this URL does not accept your key as input. It only accepts grant_type, code (which is ignored when grant_type is not 'authorization_code') and refresh_token.
Try this instead:
procedure TForm1.Button2Click(Sender: TObject);
var
l_Response: string;
l_PostData: TStringList;
l_IRESAccessToken: TIRESAccessToken;
begin
IdHTTP2.Request.ContentType := 'application/x-www-webform-urlencoded';
l_PostData := TStringList.Create;
try
l_PostData.Add('grant_type=refresh_token');
l_PostData.Add('refresh_token=' + m_IRESAuth.RefreshToken);
try
l_Response := IdHTTP2.Post('https://securetoken.googleapis.com/v1/token', l_PostData);
l_IRESAccessToken := TJson.JsonToObject<TIRESAccessToken>(l_Response);
except
on E: Exception do
ShowMessage('Error on request: '#13#10 + e.Message);
end;
finally
l_PostData.Free;
end;
end;
That being said, after reading the above documentation, I don't believe you can send an idToken from https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword to https://securetoken.googleapis.com/v1/token as a refresh_token. I think you need to send it as an authorization_code instead, which then gives you a refresh_token. So try adding that extra step, if you have not already done so.
Sorry for the late response and thank you for the answer.
After consultation with the Main-API-Developer, he told me that they are using Firebase in the background. So the API request für Google is different.
https://cloud.google.com/identity-platform/docs/use-rest-api#section-sign-in-email-password
https://cloud.google.com/identity-platform/docs/use-rest-api#section-refresh-token
With this missing information the request works fine
procedure TForm1.Button2Click(Sender: TObject);
var
l_Response: string;
l_IRESAccessToken: TIRESAccessToken;
l_Json: string;
l_JsonToSend: TStringStream;
begin
l_Json := '{"grant_type": "refresh_token","refresh_token": "'+ m_IRESAuth.RefreshToken+ '"}';
l_JsonToSend := TStringStream.Create(l_Json, TEncoding.UTF8);
try
try
l_Response := IdHTTP2.Post('https://securetoken.googleapis.com/v1/token?key=XXX, l_JsonToSend);
l_IRESAccessToken := TJson.JsonToObject<TIRESAccessToken>(l_Response);
except
on E: Exception do
ShowMessage('Error on request: '#13#10 + e.Message);
end;
finally
l_JsonToSend.Free;
end;
end;

IdHTTP.Put Error: HTTP/1.1405 Method Not Allowed

In Delphi XE2, I am trying to upload the lines of a memo to a file on my webspace with IdHTTP.Put:
procedure TForm1.btnUploadClick(Sender: TObject);
var
StringToUpload: TStringStream;
begin
StringToUpload := TStringStream.Create('');
try
StringToUpload.WriteString(memo.Lines.Text);
// Error: HTTP/1.1 405 Method Not Allowed.
IdHTTP1.Put(edtOnlineFile.Text, StringToUpload);
finally
StringToUpload.Free;
end;
end;
But I always get this error message:
So what must I do to avoid the error and make the upload?
It means the HTTP server does not support the PUT method on that URL (if at all). There is nothing you can do about that. You will likely have to upload your data another way, usually involving POST instead, or a completely different protocol, like FTP.
BTW, when using TStringStream like this, don't forget to reset the Position if you use the WriteString() method:
StringToUpload.WriteString(memo.Lines.Text);
StringToUpload.Position := 0;
Otherwise, use the constructor instead:
StringToUpload := TStringStream.Create(memo.Lines.Text);
Thanks for the above code, here is perhaps a little more information with a little helper function to assist with that Stream constructor which I found works for any string you pass through, even it contains binary stuff.
//Helper function to make JSON string correct for processing with POST / GET
function StringToStream(const AString: string): TStream;
begin
Result := TStringStream.Create(AString);
end;
//somewhere in your code, I am posting to Spring REST, encoding must be utf-8
IdHTTP1.Request.ContentType := 'application/json'; //very important
IdHTTP1.Request.ContentEncoding := 'utf-8'; //which encoding?
response := IdHTTP1.Put(URL, StringToStream(body)); //response,URL,body are declared as String

communication with rpc service using indy HTTP client and Superobject DELPHI http 400 error code

Ok im stumped the following code gives me a http 400 error which tells me there is something wrong with the submitted data but i cant figure out what!:(
the client is set to encode URL (Ive tried both true/false);
procedure TForm2.Button1Click(Sender: TObject);
var
O:Isuperobject;
T:Tstringlist;
begin
T := Tstringlist.Create;
O := SO('{"jsonrpc": "1.0", "method": getinfo, "params": "[]" }');
t.Add(o.AsString) ;
idhttp1.Request.ContentType := '"application/json"';
memo1.lines.Add( idhttp1.post('http://127.0.0.1:8332/', T ) )
end;
end.
Maybe im to Tired who knows but this is driving off the wall :\
Documentation on the RPC client
https://en.bitcoin.it/wiki/API_reference_%28JSON-RPC%29
USES:
Superobject Link : http://www.progdigy.com/?page_id=6
400 means "Bad Request". You are sending data that the server cannot process. Try sending your JSON data using TIdHTTP.Post(TStream) instead of TIdHTTP.Post(TStrings). The TStrings version encodes the string data in a way that will likely alter the JSON data so it is not valid JSON anymore. That version of Post() is meant for 'application/x-www-form-urlencoded' requests instead.
Try this:
procedure TForm2.Button1Click(Sender: TObject);
var
O: Isuperobject;
Strm: TStringStream;
begin
O := SO('{"jsonrpc": "1.0", "method": getinfo, "params": "[]" }');
Strm := TStringStream.Create(O.AsString);
try
IdHTTP1.Request.ContentType := 'application/json';
Memo1.Lines.Add(IdHTTP1.Post('http://127.0.0.1:8332/', Strm));
finally
Strm.Free;
end;
end;
The documentation says
Basic access authentication must be
used when communicating with it, and,
for security, by default, the server
only accepts connections from other
processes on the same machine.
So your code needs to set the Username, Password and BasicAuthentication properties of the Indy Request Object.
(A missing authentication normally causes a HTTP 401 (Unauthorized) instead of a 400.)

Getting Response from TIdHttp with Error Code 400

I have been writing a Delphi library for StackApps API.
I have run into a problem with Indy. I am using the version that ships with Delphi 2010.
If you pass invalid parameters to one of the StackApps API it will return a HTTP Error Code 400 and then in the response it will contain a JSON object with more details.
By visiting http://api.stackoverflow.com/0.8/stats/?Key=BadOnPurpose in Chrome Browser you can see an Example. I.E. and Firefox hide the JSON.
Using WireShark I can see that the JSON object is returned using the code below, but I am unable to access it using Indy.
For this test code I dropped a TIdHttp component on the form and placed the following code in a button click.
procedure TForm10.Button2Click(Sender: TObject);
var
SS : TStringStream;
begin
SS := TStringStream.Create;
IdHTTP1.Get('http://api.stackoverflow.com/0.8/stats/?Key=BadOnPurpose',SS,[400]);
Memo1.Lines.Text := SS.DataString;
SS.Free;
end;
I passed [400] so that it would not raise the 400 exception. It is as if Indy stopped reading the response. As the contents of Memo1 are empty.
I am looking for a way to get the JSON Details.
Remove the AIgnoreReplies parameter value from your call to Get(). Let it raise the exception normally. The JSON text you are looking for is in the EIdHTTPProtocolException.ErrorMessage property. For example:
procedure TForm10.Button2Click(Sender: TObject);
begin
try
Memo1.Lines.Text := IdHTTP1.Get('http://api.stackoverflow.com/0.8/stats/?Key=BadOnPurpose');
except
on E: EIdHTTPProtocolException do begin
if E.ErrorCode = 400 then
Memo1.Lines.Text := E.ErrorMessage
else
raise;
end;
end;
end;

Resources