Ignore errors when sending an error response to the client before closing a connection

This commit is contained in:
Kovid Goyal 2015-05-20 10:47:03 +05:30
parent ea7cac5612
commit 97c59251bb
3 changed files with 44 additions and 36 deletions

View File

@ -18,7 +18,7 @@ from calibre.constants import __version__
from calibre.srv.errors import ( from calibre.srv.errors import (
MaxSizeExceeded, NonHTTPConnRequest, HTTP404, IfNoneMatch, BadChunkedInput) MaxSizeExceeded, NonHTTPConnRequest, HTTP404, IfNoneMatch, BadChunkedInput)
from calibre.srv.respond import finalize_output, generate_static_output from calibre.srv.respond import finalize_output, generate_static_output
from calibre.srv.utils import MultiDict, http_date from calibre.srv.utils import MultiDict, http_date, socket_errors_to_ignore
HTTP1 = 'HTTP/1.0' HTTP1 = 'HTTP/1.0'
HTTP11 = 'HTTP/1.1' HTTP11 = 'HTTP/1.1'
@ -149,6 +149,14 @@ def http_communicate(conn):
def repr_for_pair(pair): def repr_for_pair(pair):
return pair.repr_for_log() if getattr(pair, 'started_request', False) else 'None' return pair.repr_for_log() if getattr(pair, 'started_request', False) else 'None'
def simple_response(pair, code, msg=''):
if pair and not pair.sent_headers:
try:
pair.simple_response(code, msg=msg)
except socket.error as e:
if e.errno not in socket_errors_to_ignore:
raise
try: try:
while True: while True:
# (re)set pair to None so that if something goes wrong in # (re)set pair to None so that if something goes wrong in
@ -177,8 +185,7 @@ def http_communicate(conn):
if (not request_seen) or (pair and pair.started_request): if (not request_seen) or (pair and pair.started_request):
# Don't bother writing the 408 if the response # Don't bother writing the 408 if the response
# has already started being written. # has already started being written.
if pair and not pair.sent_headers: simple_response(pair, httplib.REQUEST_TIMEOUT)
pair.simple_response(httplib.REQUEST_TIMEOUT)
except NonHTTPConnRequest: except NonHTTPConnRequest:
raise raise
except socket.error: except socket.error:
@ -188,19 +195,16 @@ def http_communicate(conn):
except MaxSizeExceeded as e: except MaxSizeExceeded as e:
conn.server_loop.log.warn('Too large request body (%d > %d) for request:' % (e.size, e.limit), repr_for_pair(pair)) conn.server_loop.log.warn('Too large request body (%d > %d) for request:' % (e.size, e.limit), repr_for_pair(pair))
# Can happen if the request uses chunked transfer encoding # Can happen if the request uses chunked transfer encoding
if pair and not pair.sent_headers: simple_response(pair, httplib.REQUEST_ENTITY_TOO_LARGE,
pair.simple_response(httplib.REQUEST_ENTITY_TOO_LARGE, "The entity sent with the request exceeds the maximum "
"The entity sent with the request exceeds the maximum " "allowed bytes (%d)." % pair.max_request_body_size)
"allowed bytes (%d)." % pair.max_request_body_size)
except BadChunkedInput as e: except BadChunkedInput as e:
conn.server_loop.log.warn('Bad chunked encoding (%s) for request:' % as_unicode(e.message), repr_for_pair(pair)) conn.server_loop.log.warn('Bad chunked encoding (%s) for request:' % as_unicode(e.message), repr_for_pair(pair))
if pair and not pair.sent_headers: simple_response(pair, httplib.BAD_REQUEST,
pair.simple_response(httplib.BAD_REQUEST, 'Invalid chunked encoding for request body: %s' % as_unicode(e.message))
'Invalid chunked encoding for request body: %s' % as_unicode(e.message))
except Exception: except Exception:
conn.server_loop.log.exception('Error serving request:', pair.repr_for_log() if getattr(pair, 'started_request', False) else 'None') conn.server_loop.log.exception('Error serving request:', pair.repr_for_log() if getattr(pair, 'started_request', False) else 'None')
if pair and not pair.sent_headers: simple_response(pair, httplib.INTERNAL_SERVER_ERROR)
pair.simple_response(httplib.INTERNAL_SERVER_ERROR)
class FixedSizeReader(object): # {{{ class FixedSizeReader(object): # {{{
@ -247,7 +251,7 @@ class ChunkedReader(object): # {{{
if len(chunk) < chunk_size: if len(chunk) < chunk_size:
raise BadChunkedInput('Bad chunked encoding, chunk truncated: %d < %s' % (len(chunk), chunk_size)) raise BadChunkedInput('Bad chunked encoding, chunk truncated: %d < %s' % (len(chunk), chunk_size))
if not chunk.endswith(b'\r\n'): if not chunk.endswith(b'\r\n'):
raise BadChunkedInput('Bad chunked encoding: %r != CRLF' % chunk[:-2]) raise BadChunkedInput('Bad chunked encoding: %r != CRLF' % chunk[-2:])
self.rbuf.seek(0, os.SEEK_END) self.rbuf.seek(0, os.SEEK_END)
self.bytes_read += chunk_size self.bytes_read += chunk_size
if chunk_size == 2: if chunk_size == 2:

View File

@ -6,7 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
import socket, os, errno, ssl, time, sys import socket, os, ssl, time, sys
from operator import and_ from operator import and_
from Queue import Queue, Full from Queue import Queue, Full
from threading import Thread, current_thread, Lock from threading import Thread, current_thread, Lock
@ -15,31 +15,10 @@ from io import DEFAULT_BUFFER_SIZE, BytesIO
from calibre.srv.errors import NonHTTPConnRequest, MaxSizeExceeded from calibre.srv.errors import NonHTTPConnRequest, MaxSizeExceeded
from calibre.srv.http import http_communicate from calibre.srv.http import http_communicate
from calibre.srv.opts import Options from calibre.srv.opts import Options
from calibre.srv.utils import socket_errors_to_ignore, socket_error_eintr, socket_errors_nonblocking
from calibre.utils.socket_inheritance import set_socket_inherit from calibre.utils.socket_inheritance import set_socket_inherit
from calibre.utils.logging import ThreadSafeLog from calibre.utils.logging import ThreadSafeLog
def error_codes(*errnames):
''' Return error numbers for error names, ignoring non-existent names '''
ans = {getattr(errno, x, None) for x in errnames}
ans.discard(None)
return ans
socket_error_eintr = error_codes("EINTR", "WSAEINTR")
socket_errors_to_ignore = error_codes( # errors indicating a closed connection
"EPIPE",
"EBADF", "WSAEBADF",
"ENOTSOCK", "WSAENOTSOCK",
"ETIMEDOUT", "WSAETIMEDOUT",
"ECONNREFUSED", "WSAECONNREFUSED",
"ECONNRESET", "WSAECONNRESET",
"ECONNABORTED", "WSAECONNABORTED",
"ENETRESET", "WSAENETRESET",
"EHOSTDOWN", "EHOSTUNREACH",
)
socket_errors_nonblocking = error_codes(
'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK')
class SocketFile(object): # {{{ class SocketFile(object): # {{{
"""Faux file object attached to a socket object. Works with non-blocking """Faux file object attached to a socket object. Works with non-blocking
sockets, unlike the fileobject created by socket.makefile() """ sockets, unlike the fileobject created by socket.makefile() """

View File

@ -6,6 +6,7 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2015, Kovid Goyal <kovid at kovidgoyal.net>'
import errno
from urlparse import parse_qs from urlparse import parse_qs
import repr as reprlib import repr as reprlib
from email.utils import formatdate from email.utils import formatdate
@ -83,3 +84,27 @@ class MultiDict(dict): # {{{
def pretty(self, leading_whitespace=''): def pretty(self, leading_whitespace=''):
return leading_whitespace + ('\n' + leading_whitespace).join('%s: %s' % (k, v) for k, v in self.items()) return leading_whitespace + ('\n' + leading_whitespace).join('%s: %s' % (k, v) for k, v in self.items())
# }}} # }}}
def error_codes(*errnames):
''' Return error numbers for error names, ignoring non-existent names '''
ans = {getattr(errno, x, None) for x in errnames}
ans.discard(None)
return ans
socket_error_eintr = error_codes("EINTR", "WSAEINTR")
socket_errors_to_ignore = error_codes( # errors indicating a closed connection
"EPIPE",
"EBADF", "WSAEBADF",
"ENOTSOCK", "WSAENOTSOCK",
"ETIMEDOUT", "WSAETIMEDOUT",
"ECONNREFUSED", "WSAECONNREFUSED",
"ECONNRESET", "WSAECONNRESET",
"ECONNABORTED", "WSAECONNABORTED",
"ENETRESET", "WSAENETRESET",
"EHOSTDOWN", "EHOSTUNREACH",
)
socket_errors_nonblocking = error_codes(
'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK')