self.scope['user'] in Django channels keeps showing up as AnonymousUser - django

When I log in to my front end, my self.scope['user'] call in my friends.consumer.py in django channels returns AnonymousUser but when login and call self.scope['user'] in my chat.consumer.py it shows up as the logged in user. For some reason the scope['user'] in one of my apps work and the other one does not. I do not understand what the problem is. I have the routing set up in my django project as this
routing.py
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
import chat.routing
import friends.routing
application = ProtocolTypeRouter ({
'websocket': AuthMiddlewareStack(
URLRouter(
friends.routing.websocket_urlpatterns + chat.routing.websocket_urlpatterns
)
)
})
I structure my second consumer.py similarly to my first consumer.py.
This is my consumer.py where the scope['user'] works (I didn't need the scope['user'] in the first consumer but I just wanted to test to see if it works
chat.consumer.py
class ChatConsumer(WebsocketConsumer):
...
def connect(self):
print(self.scope['user'])
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = 'chat_%s' % self.room_name
# Join room group
async_to_sync (self.channel_layer.group_add)(
self.room_group_name,
self.channel_name
)
self.accept()
This code is the consumer where my scope['user'] shows up as anonymous user even after I have logged in.
friends.consumers.py
class FriendRequestConsumer(JsonWebsocketConsumer):
def connect(self):
user = self.scope['user']
grp = 'notifications_{}'.format(user.username)
self.accept()
async_to_sync(self.channel_layer.group_add(grp, self.channel_name))
Here are also my routing.py for each app
friends.routing.py
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'^ws/friend-request-notification/$', consumers.FriendRequestConsumer),
]
chat.routing.py
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer),
]
I was able to connected to the websocket for both of them in my reactjs frontend. I know that the AuthMiddlewareStack allows us to pull the scope['user'], I just don't understand why one works and one does. Can it be that I did not connect to the websocket properly in the frontend or I am missing something on one of my consumers? I appreciate the help and thanks in advance.
For connecting to the websocket in my js, I made a chat.js file and a notifications.js
chat.js
class Chat extends React.Component{
...
initialiseChat() {
this.waitForSocketConnection(()=> {
WebSocketInstance.fetchMessages(
this.props.username,
this.props.match.params.id
)
})
WebSocketInstance.connect(this.props.match.params.id)
}
constructor(props){
super(props)
this.initialiseChat()
}
notifications.js
class Notifications extends React.Component{
...
initialiseNotification(){
this.waitForSocketConnection(() => {
NotificationWebSocketInstance.fetchFriendRequests(
this.props.id
)
})
NotificationWebSocketInstance.connect()
}
constructor(props){
super(props)
this.initialiseNotification()
}
Here are my websocket actions:
webosocket.js (this connect function gets called in the chat.js)
class WebSocketService {
...
connect(chatUrl) {
const path ='ws://127.0.0.1:8000/ws/chat/'+chatUrl+'/';
console.log(path)
this.socketRef = new WebSocket(path)
this.socketRef.onopen = () => {
console.log('websocket open')
}
...
const WebSocketInstance = WebSocketService.getInstance();
export default WebSocketInstance;
Here is the websocket for the notification.js
notificationWebsocket.js
class WebSocketNotifications {
...
connect(){
const path = 'ws://127.0.0.1:8000/ws/friend-request-notification/'
console.log(path)
this.socketRef = new WebSocket(path)
this.socketRef.onopen = () =>{
console.log('websocket open')
}
...
const NotificationWebSocketInstance =
WebSocketNotifications.getInstance();
export default NotificationWebSocketInstance;
And here are the
routes.js
class BaseRouter extends React.Component {
<Route exact path = '/chat/:id' render={(props) => <Chat {...props}
{...this.props} isAuthenticated={this.props.isAuthenticated} />}
/>
<Route exact path = '/notifications/' render={(props) =>
<Notifications {...props} {...this.props} isAuthenticated=
{this.props.isAuthenticated} />} />

You can see between this two re_path
websocket_urlpatterns = [
re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer),
]
you used (?P<room_name>\w+) for chatconsumers.
websocket_urlpatterns = [
re_path(r'^ws/friend-request-notification/$', consumers.FriendRequestConsumer),
]
But in case of FriendRequestConsumer you didn't used. In my case I had same problem and I have added a room name for no reason and can pass any string in that room name . But anyhow i can access self.scope['user'] now.
my asgi.py is like that
ws_patterns=[
re_path(r'ws/notification/(?P<room_name>\w+)/$', NotificationConsumer.as_asgi()) # NEW CHANGE
# path('ws/notification/', NotificationConsumer.as_asgi()), # as_PREVIOUS
]
application=ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AllowedHostsOriginValidator(
AuthMiddlewareStack(
URLRouter(ws_patterns)
),
),
})

There must be some issues with your token middleware

Although this might not be the source of the problem in this instance, this might help somebody stumbling across this question like me.
My token authentication system used django_rest_knox and the authentication and login procedure were working well up until trying to access the user value in self.scope['user'] which was returning AnonymousUser.
Turns out it was as simple as adding the login method from django.
# api.py
from django.contrib.auth import login
class LoginAPI(generics.GenericAPIView):
permission_classes = (permissions.AllowAny, )
serializer_class = LoginSerializer
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
user = serializer.validated_data
login(request, user) # <- This was missing
return Response({
"user": UserSerializer(user,
context=self.get_serializer_context()).data,
"token": AuthToken.objects.create(user)[1]
})

consumers.FriendRequestConsumer-> consumers.FriendRequestConsumer.as_asgi()
must
websocket_urlpatterns = [
re_path(r'^ws/friend-request-notification/$', consumers.FriendRequestConsumer.as_asgi()),
]

Related

Apollo client subscription pass JWT token handled by Django Channels middleware

I use Graphql subscriptions with Apollo client on a Vue3 app using Django graphQL Channels and DjangoGraphqlJWT packages in my backend app.
I'm trying to pass a JWT token on the Apollo subscriptions via the connectionParams.
Following this solution. I implemented a Middleware. However Apollo is passing the connectionParams as a payload. I can't find a way to access the payload at the Middleware level, but only on the consumer.
I could access the query string property from the scope argument in the middleware. However, I can't find a way to pass a query argument after the subscription is initiated.
CLIENT SIDE:
import { setContext } from "apollo-link-context";
import { Storage } from "#capacitor/storage";
import {
ApolloClient,
createHttpLink,
InMemoryCache,
split,
} from "#apollo/client/core";
import { getMainDefinition } from "#apollo/client/utilities";
import { WebSocketLink } from "#apollo/client/link/ws";
const authLink = setContext(async (_: any, { headers }: any) => {
const { value: authStr } = await Storage.get({ key: "auth" });
let token;
if (authStr) {
const auth = JSON.parse(authStr);
token = auth.token;
}
// return the headers to the context so HTTP link can read them
return {
headers: {
...headers,
authorization: token ? `JWT ${token}` : null,
},
};
});
const httpLink = createHttpLink({
uri: process.env.VUE_APP_GRAPHQL_URL || "http://0.0.0.0:8000/graphql",
});
const wsLink = new WebSocketLink({
uri: process.env.VUE_APP_WS_GRAPHQL_URL || "ws://0.0.0.0:8000/ws/graphql/",
options: {
reconnect: true,
connectionParams: async () => {
const { value: authStr } = await Storage.get({ key: "auth" });
let token;
if (authStr) {
const auth = JSON.parse(authStr);
token = auth.token;
console.log(token); // So far so good the token is logged.
return {
token: token,
};
}
return {};
},
},
});
const link = split(
// split based on operation type
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === "OperationDefinition" &&
definition.operation === "subscription"
);
},
wsLink,
httpLink
);
const cache = new InMemoryCache();
export default new ApolloClient({
// #ts-ignore
link: authLink.concat(link),
cache,
});
BACKEND:
asgy.py
from tinga.routing import MyGraphqlWsConsumer
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from tinga.channels_middleware import JwtAuthMiddlewareStack
import os
from django.core.asgi import get_asgi_application
from django.conf.urls import url
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tinga.settings')
application = get_asgi_application()
# import websockets.routing
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": JwtAuthMiddlewareStack(
URLRouter([
url(r"^ws/graphql/$", MyGraphqlWsConsumer.as_asgi()),
])
),
})
channels_middleware.py
#database_sync_to_async
def get_user(email):
try:
user = User.objects.get(email=email)
return user
except User.DoesNotExist:
return AnonymousUser()
class JwtAuthMiddleware(BaseMiddleware):
def __init__(self, inner):
self.inner = inner
async def __call__(self, scope, receive, send):
# Close old database connections to prevent usage of timed out connections
close_old_connections()
# Either find a way to get the payload from Apollo in order to get the token.
# OR
# Pass pass the token in query string in apollo when subscription is initiated.
# print(scope) # query_string, headers, etc.
# Get the token
# decoded_data = jwt_decode(payload['token'])
# scope["user"] = await get_user(email=decoded_data['email'])
return await super().__call__(scope, receive, send)
def JwtAuthMiddlewareStack(inner):
return JwtAuthMiddleware(AuthMiddlewareStack(inner))
As far as I understand, I can only access query string / URL params in the Middleware and not the Apollo payload. Would it be possible to pass the token for now in the query string? However since the token might not exist when Apollo client is provided, it needs to be reevaluated like the connectionParams.
Any workaround?
I managed to get the token in the consumer payload and inject the user into the context.
from tinga.schema import schema
import channels_graphql_ws
from channels.db import database_sync_to_async
from django.contrib.auth.models import AnonymousUser
from graphql_jwt.utils import jwt_decode
from core.models import User
from channels_graphql_ws.scope_as_context import ScopeAsContext
#database_sync_to_async
def get_user(email):
try:
user = User.objects.get(email=email)
return user
except User.DoesNotExist:
return AnonymousUser()
class MyGraphqlWsConsumer(channels_graphql_ws.GraphqlWsConsumer):
"""Channels WebSocket consumer which provides GraphQL API."""
schema = schema
# Uncomment to send keepalive message every 42 seconds.
# send_keepalive_every = 42
# Uncomment to process requests sequentially (useful for tests).
# strict_ordering = True
async def on_connect(self, payload):
"""New client connection handler."""
# You can `raise` from here to reject the connection.
print("New client connected!")
# Create object-like context (like in `Query` or `Mutation`)
# from the dict-like one provided by the Channels.
context = ScopeAsContext(self.scope)
if 'token' in payload:
# Decode the token
decoded_data = jwt_decode(payload['token'])
# Inject the user
context.user = await get_user(email=decoded_data['email'])
else:
context.user = AnonymousUser
And then passing the token in the connectionParams
const wsLink = new WebSocketLink({
uri: process.env.VUE_APP_WS_GRAPHQL_URL || "ws://0.0.0.0:8000/ws/graphql/",
options: {
reconnect: true,
connectionParams: async () => {
const { value: authStr } = await Storage.get({ key: "auth" });
let token;
if (authStr) {
const auth = JSON.parse(authStr);
token = auth.token;
console.log(token); // So far so good the token is logged.
return {
token: token,
};
}
return {};
},
},
});

Using Websocket in Django View Not Working

Problem Summary
I am sending data to a front-end (React component) using Django and web-sockets. When I run the app and send the data from my console everything works. When I use a button on the front-end to trigger a Django view that runs the same function, it does not work and generates a confusing error message.
I want to be able to click a front-end button which begins sending the data to the websocket.
I am new to Django, websockets and React and so respectfully ask you to be patient.
Overview
Django back-end and React front-end connected using Django Channels (web-sockets).
User clicks button on front-end, which does fetch() on Django REST API end-point.
[NOT WORKING] The above endpoint's view begins sending data through the web-socket.
Front-end is updated with this value.
Short Error Description
The error Traceback is long, so it is included at the end of this post. It begins with:
Internal Server Error: /api/run-create
And ends with:
ConnectionResetError: [WinError 10054] An existing connection was forcibly closed by the remote host
What I've Tried
Sending Data Outside The Django View
The function below sends data to the web-socket.
Works perfectly when I run it in my console - front-end updates as expected.
Note: the same function causes the attached error when run from inside the Django view.
import json
import time
import numpy as np
import websocket
def gen_fake_path(num_cities):
path = list(np.random.choice(num_cities, num_cities, replace=False))
path = [int(num) for num in path]
return json.dumps({"path": path})
def fake_run(num_cities, limit=1000):
ws = websocket.WebSocket()
ws.connect("ws://localhost:8000/ws/canvas_data")
while limit:
path_json = gen_fake_path(num_cities)
print(f"Sending {path_json} (limit: {limit})")
ws.send(path_json)
time.sleep(3)
limit -= 1
print("Sending complete!")
ws.close()
return
Additional Detail
Relevant Files and Configuration
consumer.py
class AsyncCanvasConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.group_name = "dashboard"
await self.channel_layer.group_add(self.group_name, self.channel_name)
await self.accept()
async def disconnect(self, close_code):
await self.channel_layer.group_discard(self.group_name, self.channel_name)
async def receive(self, text_data=None, bytes_data=None):
print(f"Received: {text_data}")
data = json.loads(text_data)
to_send = {"type": "prep", "path": data["path"]}
await self.channel_layer.group_send(self.group_name, to_send)
async def prep(self, event):
send_json = json.dumps({"path": event["path"]})
await self.send(text_data=send_json)
Relevant views.py
#api_view(["POST", "GET"])
def run_create(request):
serializer = RunSerializer(data=request.data)
if not serializer.is_valid():
return Response({"Bad Request": "Invalid data..."}, status=status.HTTP_400_BAD_REQUEST)
# TODO: Do run here.
serializer.save()
fake_run(num_cities, limit=1000)
return Response(serializer.data, status=status.HTTP_200_OK)
Relevant settings.py
WSGI_APPLICATION = 'evolving_salesman.wsgi.application'
ASGI_APPLICATION = 'evolving_salesman.asgi.application'
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer"
}
}
Relevant routing.py
websocket_url_pattern = [
path("ws/canvas_data", AsyncCanvasConsumer.as_asgi()),
]
Full Error
https://pastebin.com/rnGhrgUw
EDIT: SOLUTION
The suggestion by Kunal Solanke solved the issue. Instead of using fake_run() I used the following:
layer = get_channel_layer()
for i in range(10):
path = list(np.random.choice(4, 4, replace=False))
path = [int(num) for num in path]
async_to_sync(layer.group_send)("dashboard", {"type": "prep", "path": path})
time.sleep(3)
Rather than creating a new connection from same server to itself , I'd suggest you to use the get_channel_layer utitlity .Because you are in the end increasing the server load by opening so many connections .
Once you get the channel layer , you can simply do group send as we normally do to send evnets .
You can read more about here
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
def media_image(request,chat_id) :
if request.method == "POST" :
data = {}
if request.FILES["media_image"] is not None :
item = Image.objects.create(owner = request.user,file=request.FILES["media_image"])
message=Message.objects.create(item =item,user=request.user )
chat = Chat.objects.get(id=chat_id)
chat.messages.add(message)
layer = get_channel_layer()
item = {
"media_type": "image",
"url" : item.file.url,
"user" : request.user.username,
'caption':item.title
}
async_to_sync(layer.group_send)(
'chat_%s'%str(chat_id),
#this is the channel group name,which is defined inside your consumer"
{
"type":"send_media",
"item" : item
}
)
return HttpResponse("media sent")
In the error log, I can see that the handshake succeded for the first iteration and failed for 2nd . You can check that by printing something in the for loop . If that's the case the handshake most probably failed due to mulitple connections . I don't know how many connections the Inmemrorycache supports from same origin,but that can be reason that the 2nd connection is getting diconnected . You can get some idea in channel docs.Try using redis if you don't want to change your code,its pretty easy if you are using linux .

Not able to get Login user in Django Channels using WebsocketConsumer

I am not able to get login user in Django channels. I am using AuthMiddlewareStack but still facing the issue. Django channels error. Using WebsocketConsumer but not able to get current logged in user
Consumers.py
class TableData(WebsocketConsumer):
def connect(self):
print(self.scope["user"])
self.group_name='tableData'
async_to_sync(self.channel_layer.group_add)(
self.group_name,
self.channel_name
)
self.accept()
data = Game.get_games(1)
self.send(text_data=json.dumps({
'payload': data
}))
routing.py
ws_pattern= [
path('ws/tableData/',consumers.TableData),
path('ws/room/' , consumers.Room),
path('ws/game/room/<room_name>' , consumers.ChatConsumer)
]
application= ProtocolTypeRouter(
{
'websocket':AuthMiddlewareStack(URLRouter(ws_pattern))
}
)
``

Django Channels consumer consuming 1 call twice

I am using a combination of DRF 3.11.0 and Channels 2.4.0 to implement a backend, and it is hosted on Heroku on 1 dyno with a Redis resource attached. I have a socket on my React frontend that successfully sends/received from the backend server.
I am having an issues where any message sent back to the front end over the socket is being sent twice. I have confirmed through console.log that the front end is only pinging the back end once. I can confirm through print() inside of the API call that the function is only calling async_to_sync(channel_layer.group_send) once as well. The issue is coming from my consumer - when I use print(self.channel_name) inside of share_document_via_videocall(), I can see that two instances with different self.channel_names are being called (specific.AOQenhTn!fUybdYEsViaP and specific.AOQenhTn!NgtWxuiHtHBw. It seems like the consumer has connected to two separate channels, but I'm not sure why. When I put print() statements in my connect() I only see it go through the connect process once.
How do I ensure that I am only connected to one channel?
in settings.py:
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
#"hosts": [('127.0.0.1', 6379)],
"hosts": [(REDIS_HOST)],
},
},
}
Consumer:
import json
from asgiref.sync import async_to_sync
from channels.db import database_sync_to_async
from channels.generic.websocket import AsyncWebsocketConsumer
from rest_framework.authtoken.models import Token
from django.contrib.auth.models import AnonymousUser
from .exceptions import ClientError
import datetime
from django.utils import timezone
class HeaderConsumer(AsyncWebsocketConsumer):
async def connect(self):
print("connecting")
await self.accept()
print("starting")
print(self.channel_name)
await self.send("request_for_token")
async def continue_connect(self):
print("continuing")
print(self.channel_name)
await self.get_user_from_token(self.scope['token'])
await self.channel_layer.group_add(
"u_%d" % self.user['id'],
self.channel_name,
)
#... more stuff
async def disconnect(self, code):
await self.channel_layer.group_discard(
"u_%d" % self.user['id'],
self.channel_name,
)
async def receive(self, text_data):
text_data_json = json.loads(text_data)
if 'token' in text_data_json:
self.scope['token'] = text_data_json['token']
await self.continue_connect()
async def share_document_via_videocall(self, event):
# Send a message down to the client
print("share_document received")
print(event)
print(self.channel_name)
print(self.user['id'])
await self.send(text_data=json.dumps(
{
"type": event['type'],
"message": event["message"],
},
))
#database_sync_to_async
def get_user_from_token(self, t):
try:
print("trying token" + t)
token = Token.objects.get(key=t)
self.user = token.user.get_profile.json()
except Token.DoesNotExist:
print("failed")
self.user = AnonymousUser()
REST API call:
class ShareViaVideoChat(APIView):
permission_classes = (permissions.IsAuthenticated,)
def post(self, request, format=None):
data = request.data
recipient_list = data['recipient_list']
channel_layer = get_channel_layer()
for u in recipient_list:
if u['id'] != None:
print("sending to:")
print('u_%d' % u['id'])
async_to_sync(channel_layer.group_send)(
'u_%d' % u['id'],
{'type': 'share_document_via_videocall',
'message': {
'document': {'data': {}},
'sender': {'name': 'some name'}
}
}
)
return Response()
with respect to you getting to calls with different channel names are you sure your frontend has not connected twice to the consumer? Check in the debug console in your browser.
i get same problem with nextjs as a frontend of Django channels WebSocket server.
and after searching i found the problem related with tow things:
1- react strict mode (the request sending twice) :
to disable react strict mode in next.js , go to module name "next.config.js" , and change the value for strict mode to false , as the following :
/** #type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: false,
}
module.exports = nextConfig
2- in nextjs the code run twice (outside useEffect Hook) , one on server side and the second on the client side (which means each user will connect to websocket server twice, and got two channels name , and join to same group twice each time with different channel name . ) ,
so i changed my codes to connect with Django channels server only from client side , if you like see my full code / example , kindly visit the following URL , and note the checking code about "typeof window === "undefined":
frontend nextjs code :
https://stackoverflow.com/a/72288219/12662056
i don't know if my problem same your problem , but i hope that helpful.

Sending data to django channels groups via django views

TL;DR
Want this flow:
ws://...
websocket client 1 <-----------> websocket client 2
^
|
server (send messages via views)
So I have the following:
views.py
def alarm(request):
layer = get_channel_layer()
async_to_sync(layer.group_send)('events', {
'type': 'events.alarm',
'content': 'triggered'
})
return HttpResponse('<p>Done</p>')
consumers.py
class EventConsumer(JsonWebsocketConsumer):
def connect(self):
print('inside EventConsumer connect()')
async_to_sync(self.channel_layer.group_add)(
'events',
self.channel_name
)
self.accept()
def disconnect(self, close_code):
print('inside EventConsumer disconnect()')
print("Closed websocket with code: ", close_code)
async_to_sync(self.channel_layer.group_discard)(
'events',
self.channel_name
)
self.close()
def receive_json(self, content, **kwargs):
print('inside EventConsumer receive_json()')
print("Received event: {}".format(content))
self.send_json(content)
def events_alarm(self, event):
print('inside EventConsumer events_alarm()')
self.send_json(
{
'type': 'events.alarm',
'content': event['content']
}
)
in routing.py
application = ProtocolTypeRouter({
'websocket': AllowedHostsOriginValidator(
AuthMiddlewareStack(
URLRouter(
chat.routing.websocket_urlpatterns,
)
)
),
})
where websocket_urlpatterns is
websocket_urlpatterns = [
url(r'^ws/chat/(?P<room_name>[^/]+)/$', consumers.ChatConsumer),
url(r'^ws/event/$', consumers.EventConsumer),
]
urls.py
urlpatterns = [
url(r'^alarm/$', alarm, name='alarm'),
]
when I call /alarm/ , only the HTTP request is made and the message is not sent to the websocket
The following are the logs:
[2018/09/26 18:59:12] HTTP GET /alarm/ 200 [0.07, 127.0.0.1:60124]
My intention is to make django view send to a group (use case would be for a server to send notification to all connected members in the group).
What setting am I missing here.
I am running django channels 2.1.3 with redis as backend. The CHANNELS_LAYERS etc. have all been setup.
Reference Links:
sending messages to groups in django channels 2
github issue
channels docs
EDIT:
I could send the message using the websocket-client from the view
from websocket import create_connection
ws = create_connection("ws://url")
ws.send("Hello, World")
But is it possible to send without using the above (donot wan't to create a connection)?
Source code: chat-app
credits to #Matthaus Woolard for making the concept pretty clear.
So this was the problem:
The client had disconnected when I tried to send the message from the django view. This happened as the server restarted upon code change. I refreshed the browser and it started to work.
Silly mistake
So to summarize:
Add the following in connect() in case of Synchronous consumer:
async_to_sync(self.channel_layer.group_add)('events', self.channel_name)
or add this incase of Async Consumer:
await self.channel_layer.group_add('events', self.channel_name)
create a view as follows:
def alarm(request):
layer = get_channel_layer()
async_to_sync(layer.group_send)('events', {
'type': 'events.alarm',
'content': 'triggered'
})
return HttpResponse('<p>Done</p>')