Anatomy of a REST Server
This talk is about...
- NOT networking / performance
- NOT API design / data contracts
- Code, structure, design patterns
separate business logic from transport
transport
business logic
backend
HTTP Basics
> POST /posts/ HTTP/1.1
> Host: jsonplaceholder.typicode.com
> User-Agent: curl/7.51.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 17
>
{
"hello": "world"
}
Request
- method
- uri
- headers
- body
< HTTP/1.1 201 Created
< Date: Tue, 07 Feb 2017 23:21:59 GMT
< Content-Type: application/json; charset=utf-8
< Content-Length: 35
< Connection: keep-alive
< Set-Cookie: __cfduid=d699f6e2ba57404c619ee3ac866f4122b1486509718; expires=Wed, 07-Feb-18 23:21:58 GMT; path=/; domain=.typicode.com; HttpOnly
< X-Powered-By: Express
< Vary: Origin, X-HTTP-Method-Override, Accept-Encoding
< Access-Control-Allow-Credentials: true
< Cache-Control: no-cache
< Pragma: no-cache
< Expires: -1
< X-Content-Type-Options: nosniff
< Etag: W/"23-1zNmpcc43tiUIyNP55CUQQ"
< Via: 1.1 vegur
< Server: cloudflare-nginx
< CF-RAY: 32da94ce3b6e5017-DEN
<
{
"hello": "world",
"id": 101
}
Response
- status
- headers
- body
client
server
req / res
Case API
elasticsearch
Transport
Routing
endpoint
handler
- method
- uri
- request
- response
app.get '/user/:userId/tasks', TaskCtrl.activeTasks.fromHttp
app.get '/tasks/:taskId', TaskCtrl.getTask.fromHttp
app.post '/tasks/:taskId', TaskCtrl.upsert.fromHttp
app.post '/tasks', TaskCtrl.upsert.fromHttp
app.get '/user/:id/searchHistory', SearchHistoryCtrl.getRecentHistory.fromHttp
app.get '/processingRate', ProcessingRateCtrl.getRate.fromHttp
app.get '/dataProcessingTrend', DataProcessingTrendCtrl.getTrend.fromHttp
app.post '/alarms/:alarmId/status', AlarmCtrl.updateStatus.fromHttp
app.get '/alarms/:alarmId/comments', AlarmCtrl.getComments.fromHttp
app.post '/alarms/:alarmId/comments', AlarmCtrl.addComments.fromHttp
app.get '/alarms/:alarmId/properties', AlarmCtrl.getProperties.fromHttp
app.get '/alarms/:alarmIds/smartResponse/:userId', AlarmCtrl.getAlarmSmartResponses.fromHttp
app.get '/alarms/:alarmIds/smartResponse/:userId/:approve', AlarmCtrl.approveSmartResponseAction.fromHttp
app.get '/searchesForUser', SearchCtrl.getSearchesForUser.fromHttp
app.get '/searches/:searchId', SearchCtrl.getSearch.fromHttp
app.get '/aieRules/:aieRuleId', AieRuleCtrl.getAieRule.fromHttp
app.get '/logRhythmUser/:userId', UserCtrl.getUser.fromHttp
app.post '/cases/collaborators/:caseId', CollaboratorCtrl.addCollaborationGroup.fromHttp
app.post '/stats', StatCtrl.writeStat.fromHttp
app.post '/graphql', GraphQLCtrl.graphql.fromHttp
private void bindRoutes() {
threadPool(config.indexer.RequestThreads);
port(config.indexer.HttpPort);
post("/actions/zmq", (req, res) -> routeSearchRequest(req, res));
post("/actions/dataUpdate", (req, res) -> routeDataUpdateRequest(req, res));
}
r := mux.NewRouter()
r.HandleFunc("/products/{key}", GetProduct).Methods("GET")
r.HandleFunc("/products/{key}", CreateProduct).Methods("POST")
r.HandleFunc("/products/{key}", RemoveProduct).Methods("DELETE")
r.HandleFunc("/articles/{category}/", GetArticles).Methods("GET")
r.HandleFunc("/articles/{category}/", CreateArticle).Methods("POST")
r.HandleFunc("/articles/{category}/{id:[0-9]+}", GetArticle).Methods("GET")
centralized routing
@Path(ESQueryResource.endpointName)
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class ESQueryResource {
protected static final String endpointName = "esquery";
@POST
public Response ESQuery() {
return Response.status(200).build();
}
@GET
public Response ListRepos() {
return Response.status(200).build();
}
}
namespace LogRhythm.Web.Services.ServicesHost.Controllers
{
public class PingController : BaseController
{
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
public string Get()
{
if (log.IsInfoEnabled) log.Info(User.Identity.Name + ": Get");
return DateTime.UtcNow.ToString();
}
public string Post()
{
if (log.IsInfoEnabled) log.Info(User.Identity.Name + ": Get");
return DateTime.UtcNow.ToString();
}
}
}
decentralized routing
router.HandleFunc("/cases", func(res http.ResponseWriter, req *http.Request) {
}).Methods("POST")
router.HandleFunc("/cases", func(res http.ResponseWriter, req *http.Request) {
contentType := req.Header.Get("content-type")
if contentType != "application/json" {
res.WriteHeader(415)
return
}
}).Methods("POST")
router.HandleFunc("/cases", func(res http.ResponseWriter, req *http.Request) {
contentType := req.Header.Get("content-type")
if contentType != "application/json" {
res.WriteHeader(415)
return
}
body, err := ioutil.ReadAll(req.Body)
if err != nil {
res.WriteHeader(500)
return
}
}).Methods("POST")
router.HandleFunc("/cases", func(res http.ResponseWriter, req *http.Request) {
contentType := req.Header.Get("content-type")
if contentType != "application/json" {
res.WriteHeader(415)
return
}
body, err := ioutil.ReadAll(req.Body)
if err != nil {
res.WriteHeader(500)
return
}
newCase := &CaseDetails{}
err = json.Unmarshal(body, newCase)
if err != nil {
res.WriteHeader(400)
return
}
}).Methods("POST")
router.HandleFunc("/cases", func(res http.ResponseWriter, req *http.Request) {
contentType := req.Header.Get("content-type")
if contentType != "application/json" {
res.WriteHeader(415)
return
}
body, err := ioutil.ReadAll(req.Body)
if err != nil {
res.WriteHeader(500)
return
}
newCase := &CaseDetails{}
err = json.Unmarshal(body, newCase)
if err != nil {
res.WriteHeader(400)
return
}
CaseController.CreateCase(newCase)
res.WriteHeader(201)
}).Methods("POST")
handlers
Business Logic
- native data structures
- domain objects
- business rules
type ChangeCaseStatusRequest struct {
caseID int
newStatus int
}
func ChangeCaseStatus(req ChangeCaseStatusRequest) CaseDetails {
caseInstance := backend.getCaseById(req.caseID)
currentStatus := caseInstance.status
if newStatus <= currentStatus {
panic(APIError(400)
.Message("Cannot move backwards in the case workflow"))
}
caseInstance.status = req.newStatus
backend.saveCase(caseInstance)
return caseInstance
}
Backend
- Another service
- Database
- File System
- ...?
Request / Response
- stateless
- blocking
- thread-per-request
My Server!
clients!
req / res threads
My Server!
req / res threads
error handling
authentication
content negotiation
deserialize input
validate input
ACTUAL BUSINESS LOGIC!
func catchErrors(res http.ResponseWriter) {
err := recover()
if err == nil {
return
}
logger.Error("Error handling http request:\n", string(debug.Stack()))
//check to see if it's a structured API error
apiErr, ok := err.(*apiError)
if !ok {
apiErr = APIError(500)
standardErr, ok := err.(error)
if ok {
apiErr.Details(standardErr.Error())
}
}
//send the response to the client
res.WriteHeader(apiErr.StatusCode)
buffer, _ := json.Marshal(apiErr)
res.Write(buffer)
return
}
func (s *server) ServeHTTP(res http.ResponseWriter, req *http.Request) {
defer catchErrors(res)
router.ServeHTTP(res, req)
}
func ChangeCaseStatus(req ChangeCaseStatusRequest) CaseDetails {
caseInstance, err := backend.getCaseById(req.caseID)
if err != nil {
panic(APIError(500).
Message("Failed to retrieve case from database.").
Details(err.Error()))
}
if caseInstance == nil {
panic(APIError(404).
Message("A case with id " + string(req.caseID) + " does not exist"))
}
currentStatus := caseInstance.status
if newStatus <= currentStatus {
panic(APIError(400).
Message("Cannot move backwards in the case workflow"))
}
caseInstance.status = req.newStatus
err = backend.saveCase(caseInstance)
if caseInstance == nil {
panic(APIError(500).
Message("Failed to update case status.").
Details(err.Error()))
}
return caseInstance
}
public BaseResponse searchIndex(Request req, Response res) {
string body = req.body()
try {
Gson gson = new GsonBuilder().create();
Envelope envelope = gson.fromJson(envelopeString, Envelope.class);
return routeEnvelope(envelope);
} catch (JsonSyntaxException e) {
ErrorResponse errorResponse = new ErrorResponse();
errorResponse.setName("BadRequest");
errorResponse.setStatusCode(400);
errorResponse.setMessage("Invalid JSON: " + e.getMessage());
errorResponse.setDetails(getStackTrace(e));
return errorResponse;
} catch (ApiException e) {
ErrorResponse errorResponse = ErrorResponse.fromApiException(e);
return errorResponse;
} catch (Exception e) {
ErrorResponse errorResponse = new ErrorResponse();
errorResponse.setName("ServerError");
errorResponse.setStatusCode(500);
errorResponse.setMessage("Unknown server error occurred: " + e.getMessage());
errorResponse.setDetails(getStackTrace(e));
return errorResponse;
}
}
My Server!
req / res threads
shared resources & backpressure
DATA FILE
Solved Problems!
Content Negotiation
Cache Control
Anatomy of an HTTP Server
By autoric
Anatomy of an HTTP Server
- 655