Server-sent events in Rails not delivered asynchronously - api

I'm trying to write an API that delivers server-sent events using ActionController::Live::SSE in Rails 6. In order to understand how the tests would best be written, I started with essentially copying the trivial example seen here:
my_controller.rb:
class MyController < ApplicationController
include ActionController::Live
def capture
response.headers['Content-Type'] = 'text/event-stream'
sse = SSE.new(response.stream)
3.times do
sse.write({message: "Awaiting confirmation ..."})
sleep 2
end
fake_response = { #The response as hash.
"annotation_id"=>nil,
"domain"=>"some.random.com",
"id"=>2216354,
"path"=>"/flummoxer/",
"protocol"=>"https",
}
sse.write(fake_response, event: 'successful capture')
rescue => e
sse.write(e.message, event: 'something broke: ')
ensure
response.stream.close
end
end
When I send a curl request (whether I make it POST or GET) to this endpoint the response arrives all in one chunk, rather than as separate responses:
$ curl -i -X GET -H "Content-Type: application/json" -d '{"url": "https://some.random.com/flummoxer"}' http://localhost:3000/capture
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
ETag: W/"a24048695d2feca40232467f0fbb410a"
X-Request-Id: 648a5229-a43d-40d3-82fd-1c4ea6fe19cc
X-Runtime: 24.082528
Transfer-Encoding: chunked
data: {"message":"Awaiting confirmation ..."}
data: {"message":"Awaiting confirmation ..."}
data: {"message":"Awaiting confirmation ..."}
event: successful capture
data: {"annotation_id":null,"domain":"some.random.com","id":2216354,"path":"/flummoxer/","protocol":"https"}
This can more easily be seen by the fact that in my test, attempting to parse the response from my server fails:
MultiJson::ParseError: 783: unexpected token at 'data: {"message":"Awaiting confirmation ..."}
data: {"message":"Awaiting confirmation ..."}
data: {"message":"Awaiting confirmation ..."}
event: successful capture
data: {"annotation_id":null,"domain":"some.random.com","id":2216354,"path":"/flummoxer/","protocol":"https"}
'
My server is Puma, so it's not because I'm using Thin, as seen in this answer.
What am I doing wrong? I'll provide any additional information that might be of use, if you ask.
UPDATE: The answers to this question suggest adding both the -N and the Accept:text/event-stream header to the request. Doing so doesn't change the behavior I've described above -- the response to the request isn't sent until the call to response.stream.close is fired.
UPDATE 2: I've also tried hacking the SSE#write method to call broadcast() on the Mutex::ConditionVariable to force sending the message. This works, in the sense that it sends data immediately, but has the side effect of the curl request thinking that the stream is closed, and so no further messages are sent, which is not a stream.
UPDATE 3: I've also modified development.rb to include config.allow_concurrency = true, as seen here. There's no change in the behavior described above.

I ran into a similar issue with a basic 'out the book' Rails 5 SSE app. The issue turned out to be a Rack update that lead to buffering of the stream. More info here https://github.com/rack/rack/issues/1619 and fixed by including
config.middleware.delete Rack::ETag
in config/application.rb

Related

Telegram Bot API: getChatMember throws USER_ID_INVALID for valid user

I'm trying to find out if a specific User is present in a supergroup, in order to keep track of those who left.
For that, I'm calling the Bot API method getChatMember for each User and checking if their status is either Left or Kicked. However, I noticed that (recently?) I'm getting USER_ID_INVALID errors for many valid users that are either in the supergroup or have been in the past and then left. I also confirmed that those accounts are still active on Telegram.
Here's the HTTP request I'm sending:
POST https://api.telegram.org/botXXXXXXXXX:XXXXXXXXXXXXXXXXXXXXXXXX/getChatMember HTTP/1.1
Connection: Keep-Alive
Content-Type: application/json; charset=utf-8
Content-Length: 46
Host: api.telegram.org
{"chat_id":-0000000000000,"user_id":000000000}
And here's the response I'm getting:
HTTP/1.1 400 Bad Request
Server: nginx/1.12.2
Date: Fri, 20 Apr 2018 04:17:32 GMT
Content-Type: application/json
Content-Length: 74
Connection: keep-alive
Access-Control-Allow-Origin: *
Access-Control-Expose-Headers: Content-Length,Content-Type,Date,Server,Connection
{"ok":false,"error_code":400,"description":"Bad Request: USER_ID_INVALID"}
Any way I look at it, it looks like a perfectly valid request to me. And I haven't been able to find a common pattern between the users that throw this error.
What am I missing here?
EDIT: As #sean pointed out, having one of those users message the bot privately fixed the error for that particular user. But I'm absolutely sure that user was seen before because that's how I got his user ID. What could have caused the bot "forget" about him and how would I prevent this from happening in the future?
This error means your bot haven't seen this user before.
For instance, my user ID is 109780439, you can try getChatMember with #PublicTestGroup, it should response with 400 error.
And then, forward ANY of my message (e.g., this) to your bot, you will see the different result :)
You will create a variable who get your channel's result, like this:
$join : api.telegram.org/botYOURTOKEN/getchat .....
if($message && (strpos($join,'"status":"left"') or strpos($join,'"Bad Request: USER_ID_INVALID"') or strpos($join,'"status":"kicked"'))!== false) {
}

DRF JSON Token Authentication

I have to implement following features with RESTAPI:
register user
authorize user and get token
get user data by token
create user's post via token (Title, Body)
get user's posts via token
get all posts via token
user profile has to be customized - main fields are email/password
models.py:
http://pastebin.com/8V7CzrVi
serializers.py:
http://pastebin.com/W7Dn8Msn
views.py:
http://pastebin.com/L5ijkd5F
urls.py:
urlpatterns = [
url(r'^api/', include('app.urls')),
]
app.urls:
from .views import Register, UserList, UserDetail, PostList, PostDetail
from rest_framework_jwt.views import obtain_jwt_token
urlpatterns = [
url(r'^users/$', UserList.as_view(), name='user-list'),
url(r'^users/(?P<pk>[0-9]+)/$', UserDetail.as_view(), name='user-detail'),
url(r'^posts/$', PostList.as_view(), name='post-list'),
url(r'^posts/(?P<pk>[0-9]+)/$', PostDetail.as_view(), name='post-detail'),
url(r'^login/', obtain_jwt_token),
url(r'^register/$', Register.as_view()),
]
First question - please, review the code and tell, am I right in my realization? (THANKS A LOT for any corrections, hints and explanations)
Second one - how can I perform creating the post or viewing of post's list without any client code?
I mean following:
I am going into browserable API at api/register, enter email and password, hit post and get object of user
then I am going at api/login, enter email and password of the user just registered, hit post and get object with token - {'token': 'sometoken'}
then I am trying to create the post or get post's list, using httpie. In the console I enter -
http POST 127.0.0.1:8000/api/posts "Authorization: Token sometoken"
or
http GET 127.0.0.1:8000/api/posts "Authorization: Token sometoken"
and get:
HTTP/1.0 301 MOVED PERMANENTLY
Content-Type: text/html; charset=utf-8
Date: Sat, 03 Sep 2016 10:22:33 GMT
Location: http://127.0.0.1:8000/api/posts/
Server: WSGIServer/0.1 Python/2.7.11
X-Frame-Options: SAMEORIGIN
I do not understand how I can check every endpoint with using token.
Thanks!!!

curl request to HTTParty request

I have very little experience with APIs can someone please help me translate this:
$ curl -X POST -d "login_id=myemail#email.com&api_key=231421423423423423423" \
https://devapi.testapi.com/v2/authenticate/api
to something like this?:
include HTTParty
base_uri 'https://devapi.testapi.com'
def login
response = self.class.post( "/v2/authenticate/api",
:headers => { "login_id" => 'myemail#email.com', "api_key" => '231421423423423423423' }
).parsed_response
#token = response["auth_token"]
return #token
end
I'm not sure where to put login_id and api_key so that they would appear at the beginning of the request address instead of the end.
First, we need to clear up some confusion. When you send a POST request, like you did with Curl, it looks like this:
POST /v2/authenticate/api HTTP/1.1
User-Agent: curl/7.30.0
Host: localhost:8000
Accept: */*
Content-Length: 56
Content-Type: application/x-www-form-urlencoded
login_id=myemail#email.com&api_key=231421423423423423423
The first line is the request method (POST) and resource (/v2/authenticate/api) and the protocol version (HTTP/1.1). The next five lines are the headers, which always have a key (e.g. Content-Length), followed by a colon and space (:), followed by a value (56).
You'll notice that none of these headers have your data, i.e. login_id or api_key. Data doesn't go in the headers. It goes in the body, which is after the headers and an intervening blank line that tells the server, "the headers are done; everything else I send is the body."
Hopefully that will help clear up the confusion that I see here:
I'm not sure where to put login_id and api_key so that they would appear at the beginning of the request address instead of the end.
In a POST request the data is not part of the address, nor is it, to reiterate, part of the headers. It's the body.
When making a POST request with HTTParty, you could use the :body option to specify a string to use as the POST body, but in your Ruby code it looks like you'd rather use a Hash, which is the right way to go. With a Hash, you use the :query option instead of :body, and HTTParty will automatically encode the Hash's contents correctly. Using that, your code would look like this:
query_hash = { :login_id => 'myemail#email.com',
:api_key => '231421423423423423423' }
response = self.class.post("/v2/authenticate/api", :query => query_hash)

send delete request to controller in cucumber step definition

Does any of you know how to do (implement) something like this:
sample.feature
...
scenario: unauthorized user cannot delete event
Given list of events
When event is deleted
Then nothing happen
...
sample_steps.rb
...
When /^event is deleted$/ do
delete (_path_to_controller_ + "/%d" % #events.first().id)
...
Of course in this step I want to send a request according to the result of rake routes, which is something like this (I've moved resources under admin path):
rake routes
...
DELETE /admin/controller_name/:id(.:format) controller_name#destroy
...
I have been experimenting and searching internet for so long and yet I don't know how to do it :(
I've used Rack::Test in the past to send DELETE requests to an API:
When /^event is deleted$/ do
header 'Accept', 'application/json'
header 'Content-Type', 'application/json'
authorize "username", "password"
url = _path_to_controller_ + "/%d" % #events.first().id)
delete url
end
Having said that, I'm not sure I'd recommend it in your case. Is the event going to be deleted from some action in the interface such as clicking a button? If so, you should use capybara to log in and click the button. This gives you the benefit of full integration coverage and you don't have to deal with Rack::Test (not that it's a bad tool, but it's another tool).
Uff I've solved the problem.
Great Thanks to Beerlington
So in this post I will sum up my time with the problem and its solution.
Related topics and documentation
StackOverflow: HTTP basic auth for Capybara
Devise: How To: Use HTTP Basic Authentication
Background
I'm using devise gem for authentication. My goal was to check if possible is manual hacking to resource management features like delete.
Problem
Above ;D
Solution
When /^event is deleted$/ do
header 'Accept', 'application/json'
header 'Content-Type', 'application/json'
authorize "username", "password"
url = _path_to_controller_ + "/%d" % #events.first().id)
delete url
end
is not working with default devise configuration. Because it uses HTTP authentication which is disabled by default.
config/initializers/devise.rb
# Tell if authentication through HTTP Basic Auth is enabled. False by default.
# It can be set to an array that will enable http authentication only for the
# given strategies, for example, `config.http_authenticatable = [:token]` will
# enable it only for token authentication.
# config.http_authenticatable = false
So if we want to make above test working, we need to change last line to:
config.http_authenticatable = true
But the question is do we really wanna do it ?
And as a last note: header calls are optional. Records are deleted with or without them.
with them delete return with status code : 204 No Content
and without them delete return with status code : 302 Found

sending data back in response after SOAP call

So I would like to to this:
receive a POST from external url in controller and capture data
use this data in SOAP request; use response back from SOAP request to modify data and then
send data back to external client in the response to external url in step 1.
It seems I have some timing issues in my approach as the object returned to external client is empty. If I comment out my Savon code doing the SOAP call the object is complete. Here is the short version of what I am doing using Rails 3.0.9, Savon 0.9.7
1) works great
#
# get object from POST
#
string = request.body.read
#myobject = ActiveSupport::JSON.decode(string)
2) also works great (leaving out most of Savon magic as response has precisely what it should)
...
response = client.request :urn, "MySOAPOperation" do
...
3) Here is where I am a little "off"
response_hash =response.to_hash[:my_soap_operation_response]
#myobject.status = response_hash[:status].to_s
logger.info ("Status: " + #myobject.status)
#
# at this point myobject updated values are confirmed in log
#
respond_to do |format|
format.json { render :json => #myobject }
end
Now the response is correctly forwarded back to Step 1. external client but with #myobject empty. If I comment out the SOAP request and response the full #myobject is sent along with response to the client. I am thinking this is a timing issue as I also obtained the same result by commenting out the SOAP request and adding
sleep 20
in its place. Any suggestions will be greatly appreciated. Thank you for reading.
Dan
The culprit turned out to be settings on the PHP side of things.
Before (empty object returned):
curl_setopt($ch, CURLOPT_TIMEOUT, 15);
After (complete object returned):
curl_setopt($ch, CURLOPT_TIMEOUT, 60);