From bc20dc5c62ba21f4668caaef6b69a963619fd35d Mon Sep 17 00:00:00 2001 From: lixxu Date: Wed, 6 Sep 2017 19:17:52 +0800 Subject: [PATCH 01/38] use url_for for url building for static files --- docs/sanic/blueprints.md | 9 +- docs/sanic/routing.md | 31 +++ docs/sanic/static_files.md | 30 ++- sanic/app.py | 35 ++- sanic/blueprints.py | 5 + sanic/router.py | 31 ++- sanic/static.py | 9 +- tests/static/bp/decode me.txt | 1 + tests/static/bp/python.png | Bin 0 -> 11252 bytes tests/static/bp/test.file | 1 + tests/test_url_building.py | 6 +- tests/test_url_for_static.py | 446 ++++++++++++++++++++++++++++++++++ 12 files changed, 589 insertions(+), 15 deletions(-) create mode 100644 tests/static/bp/decode me.txt create mode 100644 tests/static/bp/python.png create mode 100644 tests/static/bp/test.file create mode 100644 tests/test_url_for_static.py diff --git a/docs/sanic/blueprints.md b/docs/sanic/blueprints.md index 5be33cb6..1a7c5293 100644 --- a/docs/sanic/blueprints.md +++ b/docs/sanic/blueprints.md @@ -93,7 +93,14 @@ def ignore_404s(request, exception): Static files can be served globally, under the blueprint prefix. ```python -bp.static('/folder/to/serve', '/web/path') + +# suppose bp.name == 'bp' + +bp.static('/web/path', '/folder/to/serve') +# also you can pass name parameter to it for url_for +bp.static('/web/path', '/folder/to/server', name='uploads') +app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/web/path/file.txt' + ``` ## Start and stop diff --git a/docs/sanic/routing.md b/docs/sanic/routing.md index 49b7c0b8..7a562b66 100644 --- a/docs/sanic/routing.md +++ b/docs/sanic/routing.md @@ -301,3 +301,34 @@ def handler(request): # app.url_for('handler') == '/get' # app.url_for('post_handler') == '/post' ``` + +## Build URL for static files + +You can use `url_for` for static file url building now. +If it's for file directly, `filename` can be ignored. + +```python + +app = Sanic('test_static') +app.static('/static', './static') +app.static('/uploads', './uploads', name='uploads') +app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png') + +bp = Blueprint('bp', url_prefix='bp') +bp.static('/static', './static') +bp.static('/uploads', './uploads', name='uploads') +bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png') +app.blueprint(bp) + +# then build the url +app.url_for('static', filename='file.txt') == '/static/file.txt' +app.url_for('static', name='static', filename='file.txt') == '/static/file.txt' +app.url_for('static', name='uploads', filename='file.txt') == 'uploads/file.txt' +app.url_for('static', name='best_png') == '/the_best.png' + +# blueprint url building +app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt' +app.url_for('static', name='bp.uploads', filename='file.txt') == '/bp/uploads/file.txt' +app.url_for('static', name='bp.best_png') == '/bp/static/the_best.png' + +``` diff --git a/docs/sanic/static_files.md b/docs/sanic/static_files.md index f0ce9d78..3419cad1 100644 --- a/docs/sanic/static_files.md +++ b/docs/sanic/static_files.md @@ -6,16 +6,40 @@ filename. The file specified will then be accessible via the given endpoint. ```python from sanic import Sanic +from sanic.blueprints import Blueprint + app = Sanic(__name__) # Serves files from the static folder to the URL /static app.static('/static', './static') +# use url_for to build the url, name defaults to 'static' and can be ignored +app.url_for('static', filename='file.txt') == '/static/file.txt' +app.url_for('static', name='static', filename='file.txt') == '/static/file.txt' # Serves the file /home/ubuntu/test.png when the URL /the_best.png # is requested -app.static('/the_best.png', '/home/ubuntu/test.png') +app.static('/the_best.png', '/home/ubuntu/test.png', name='best_png') + +# you can use url_for to build the static file url +# you can ignore name and filename parameters if you don't define it +app.url_for('static', name='best_png') == '/the_best.png' +app.url_for('static', name='best_png', filename='any') == '/the_best.png' + +# you need define the name for other static files +app.static('/another.png', '/home/ubuntu/another.png', name='another') +app.url_for('static', name='another') == '/another.png' +app.url_for('static', name='another', filename='any') == '/another.png' + +# also, you can use static for blueprint +bp = Blueprint('bp', url_prefix='/bp') +bp.static('/static', './static') + +# servers the file directly +bp.static('/the_best.png', '/home/ubuntu/test.png', name='best_png') +app.blueprint(bp) + +app.url_for('static', name='bp.static', filename='file.txt') == '/bp/static/file.txt' +app.url_for('static', name='bp.best_png') == '/bp/test_best.png' app.run(host="0.0.0.0", port=8000) ``` - -Note: currently you cannot build a URL for a static file using `url_for`. diff --git a/sanic/app.py b/sanic/app.py index 20c02a5c..3776c915 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -354,13 +354,13 @@ class Sanic: # Static Files def static(self, uri, file_or_directory, pattern=r'/?.+', use_modified_since=True, use_content_range=False, - stream_large_files=False): + stream_large_files=False, name='static'): """Register a root to serve files from. The input can either be a file or a directory. See """ static_register(self, uri, file_or_directory, pattern, use_modified_since, use_content_range, - stream_large_files) + stream_large_files, name) def blueprint(self, blueprint, **options): """Register a blueprint on the application. @@ -410,12 +410,32 @@ class Sanic: URLBuildError """ # find the route by the supplied view name - uri, route = self.router.find_route_by_view_name(view_name) + kw = {} + # special static files url_for + if view_name == 'static': + kw.update(name=kwargs.pop('name', 'static')) + elif view_name.endswith('.static'): # blueprint.static + kwargs.pop('name', None) + kw.update(name=view_name) - if not uri or not route: + uri, route = self.router.find_route_by_view_name(view_name, **kw) + if not (uri and route): raise URLBuildError('Endpoint with name `{}` was not found'.format( view_name)) + if view_name == 'static' or view_name.endswith('.static'): + filename = kwargs.pop('filename', None) + # it's static folder + if 'dk`%$jxm&aCH| zHSaGv&Xz5k0?IN*P$Rw-~F0AT2l5f@SOTEF!7ZllsvzjmzA zSGQ~v+wR`XyHem4^OPP1z~f89nNV=BO`?eii>nnwM$17Ky!u7ch6KE(D&u2iJ&vKF z$%&d_z(FSqONu5d%(QkmZlB7jH?Q~vUg)cLOm|TlpBxQ)Y6L#!xnAM_QTTY$@kQ=w zMqy1(iZ!F8z#o&AWdC1YwQEG5k5dAd7Yaf(FT06)JjZQ|uRVwN`pDQU?Q=G`m*u|0 zIjb5Gv}2SK;4}4929sO4ILHhQ*9+L*mxkPTWf5NI=J7fFIQ+S6@>t-E*OJ_b#>J)BT+FdtV_mRl(=xmqPqnbHA6KD)L1j8c z$tJfbZK4`r3@NS#YfWvPFZ(OwDdW}p`euITM#Zsy_S^#(mFeLs^Sq49dVO>061A4E z%-nP4xGZqf*MI}^MS9dc@SV$RrK5GX*z$6#KJ|Xab;Il6LHwU<@)&3tq4;m93x zF53f2)fBmj039^II!OkJr2N7J4+a`)VJoR?sh87M!?`e=Jg8230yA~bjE^UG`}BQL zFaNfG&8`5uwtD0#ebl)IcH>NYu>-#<(MN?U-sjJ^f@UtQcdMJ%{;Ub+BV&G@$>gl$ z`9HNx#iel74ZVJ|=LA7L3DY-0*9EIy?|+r`4UwU>qyDbcxscp2a;)%7@;h-pXWjHtO5pAyW#5&w9v|~E z99H!j(`h$D4Qapj`Q4+k*YM=&_V9Wm{Hy!3MSJSiahp}rQ6JCf^Yyvg2Kp|6zQAvU z@FttE#Yob_T9hI-_^>dDb~*m$6I)mU1;o$4kGd0X0r zsrQq?9(B+?O)?ul7r_weoDI;6@rY8F6ITE#kWnhOEkq}_Qm|(b6SXU^5LxSmYeU@^ zR8Npyx_xJYTsatS$5bCmku8O$xBM@$DH5+^`1hNy2QOviwMu1q(+%O3+|HJnw0ax} zHW&AfI-Yi4|6T<*_}dYg{_J9Q=<)@=9lu2q$b&&h;ovD~JR~T?4P2K9cd{~=*_O{& z3@1dh{>;qW-f4k1uYd|)jqSas^QVbtlCOU>J5RP6G_!LyyjcQWc#47^`E{#Yw`$6q zS#Z=2CbhQC6PJ9mSwfK+`hL_c1>Qu%1n4fx7n$0FLI&QL-48X_yG^im1Ak5{des(w za7|pcsZ4ogB&6nQqqah+D{Ty?GwC|*%oasCRI>P{VfRf|L_{H z!|4)*&VdM1H+r5FA>ytl4Rky8_Uh-M0YxE2!iB>xV9Muu-m$eEE$jbVkMr!Ol&7p= zGc%BFRN|`vQtN%}UL9*BX-inZXfuB{2Q$`vrt8Eow$>Mq4w=kcZH~Hj!1p3^5Pw|t z$7#lKzHI%pg)eqv6ln!Os~a6(3Dg(2{oBC2y|*Ol*n3-!=entmApzHIJy31TYJ`^URU5LsXK2vd1~HANcluK(W30PW`gD z{n-b1;G1kPtyEYM?Qz1wfk{$tjZ83y%EhnCrC@UD6)YyJzY*~EJa@@x{PFr2SzPY) zt4peu!D|}3&A7T$Nw~7dwjS_jlexMX00zUvxQnFjZ3!Qj%3?!7*9 z_$FM-yTby)Ut2YAjeGi=@%FX)rxof{X$j zP_)y7^oUwdofN5q&_|ygYVSd>?oo`8M661Fo+A*G?!Zg}+vYPk;QP%Sbca~!dU}0RTO=nTIEqZo9)=h4WvdhrmW3`AD`|%&HC%x} zSNnOz>;}Z24(yebi~E+S+vj=B3!bQJhk`xwRe~XqiKUG1^nOyp(Pn$pbt%lNR1ws0 zOjgH63#t?y6csN7)tivFheg6*_TkhgzoA;C(cf1>@;t?>o55X5#x_{QaX@_m}k}^u1EFf3_Rd=9n~Om~<`)ClCH7jodV32cl5EOl(i1jQ{|u_qh*~b5=bDEdXg7ME2{n&E z&XSC8$2N+s4K&lSc1hZ$7p6ZW+x4%T?L+6bsnU$nBpn@YL&Rkpz&qy02bzdQu7cqs zGZV#udjmP3d-KfR$Z092$s&ZOTnF}hm^IJ8}81fgFI%E{|Bq|f?b0^?NDyeUtY)oV; zurf%O#VCDO-Kh4%6h=f{Dt@Ia<|mnvvdjG=e;TDaV$Ph?##v(=?*S_vZGA@{u{y1| z`huQ<0te6cnYOCZKY85))#K(8vE@3^cBzXZ#~hRyk<3QmwXXBr1obN> zv-gJ*YujjcwaW2%Bx83%_xY69t>*GR9gWA!)zC>>7KwelbSv8_JScMfpeOys8aW1N zKE1B3vq&pIP?nLd9JNn6QOrv` z>h(vSBlbeW`@kgq6652EZV;D9d zGvPtg7+`#8WzdO?ttGniS6|J)n*yr1sPL#V$Ql2WFC_-;KeI8gu%4~OAqCec>(?LW zG@lcXzXYCnd~WZBe)Eb1OfqaiP)E%KtZtNevPjesL<;6c{{2P77bcbK%&5R?fw8bJ zAKPj|ng2uAh@OP(Hmz%~{u7PvilMrf_{`zHNS7g6FGz1=7vWtvUH7MUyh3Y49^dtF zt$s%^fphap#+!3}f$feLHj4!a4igVag;uMJP7@7!f5Fo9vTVdRL2!0wK4O%IbTa<& z$WhS!wzv7$)1Qa}m${=J?|acdr~ZcjZcKd>J`Wi*QAd3?ZJnn(S|I9Gw{zkQ=f+N{ zvW6bCIEFDvTtLi%UzB#a1XU)QsHugJiU?q}i4kvr8H5ddu8nsObUU!JO*|4iW4>uC z2PcO6JGaGv4-r3W2&b&RPBI)4tkMwBJ={<|kBc}MSvayctX1OYG%ABUoDpo~UiyMr zy69UA{AE-Pcm4hyCR@)v6K0aa%u{Rk%Xpx-$?NKIpo2TuuTRKf$W3hd z`bymK>gd6*D$;b|-_U)_t~IUzKnANQ#ik+)yD%+XV_h1ePzUOJ9fZu4xAH+if}0Vq zaLp=$);R*gvQ8wn))<=*7p$%QE`A0)3O)ZN_)gaze%cSWgOP(wlnhu{T?qQ&!PYiq zQS>5$Mp!o_;vtU>W&kJB>zJN5?EW(Z%L^?nPw%c&+ix_S7sCb~e-wQ6QhD{)`5jpz z-ccBSD##7C_`iw1kL1M~6oP~371o=8sC@U(NSva-41?6OcV;`6IXR(II?H%J(;f?T zQ1c&mqOb#PuhETcne6gQ{$82{1fVE|8EfiV)Pmieos_}l?}F&UX(Axn%F)Tt%Ab1A z7WTZbQz($im}-o1{To#P`fe{OC2Y?uOl0{kFrCpE&_#+3%s6 z-IxVm54^dLnFBM|hYy66tY8iq8hZoOI34cQC5+kZmnAgu=T^CH1fbTsCajsKx_R^-$vd5Vg5y9NMHtFKWEQV+Cxh#MB5c)uxhs=~fBiSeJIO^;nD?TJY2b7Fjicv2`pfH!y8T~R z#+RM&*0yQxbK9CK3hLxUO|d$o1-0mg)vi(kkrRQHMSUPI%@1RHX9)Y|K$M_wNI#K3 zzay3V=K98Y=oG7DO{_ZMpW_q~C}J3Sn1vO?DRW~~-o#9U8X`?$4^ahENx#2>-h)7^YKF5dRwdr{6SC-XX7>h4W_pRs5 zuyuz2!wN^=X)_iDy3ewa;RyO7L&7SL$eAOetBrj;|3uPpd&hP2^bratY_gD_U1flB z3siIT20n*kL2?+J@=8yC$@YDJX@K{(REey?e z6HQExBwSR~jFbRuHae!lZ9ncmrDxFEvcuiUxU4+`H!X+MT_IqFLepnbHVb6+ z4%TbqZieY+=H|NX;$vH9nXq`+Hx5B!=ZM)OP=_amiM$TlT9AHt5++}haGzy&nvNOy ztCFN4wit&|eojszQBx1I34S*S)}6Kd%_-5kAQCtmp`~kMxb)+qIk6qG%qdc-6dPY> zxCV=xA7#;ps=|*YSEkn5rK0PK=ZQeyubj_O!eEhH%{a~Yy}Md%sm?zwI$A#UJQheL+v7P*w2+=<1&K#&k~#mdzJ)MTs1-AeerS z)`dLV$3eSjjDD2k_$5Y*wUI(RGInI6!wIzRIYje$+wQgQ=y!4g_A7LBTGL!1h1z*% z6;RUSH^rMo$~n%=;fv*tu0kti&6E4qsC!{X1lVmtIy?Yia!V-4F51%Ayjv5{KV{rn zTcP(4?N7O@)$_}a*H$?mFPi`|2H8MVh^b<*@(sA5Vf)n2CHT+O8&pPk-&qf)4VV0J zd_9bJ(W13hH?+AMFc|&9$sxp;+q&*om$jODJQq1%lnUlp|4?Du(b-mq;k<EMO(RnCZy0bz_^l5+gPs0TEjU0Qb$c zXh>YJx-sYo#UB^a!DldidJos((?4MP#nD^ZtorS9(x~U8YB0vPE)@nn^>2!Ca6QDT z6tr9KcXdJ&vCkt&CYq#c|y zpdKDzfkIR(`WBK+fXrtXHgL#d*I-1yj+VAutkqP^>^jgw!d0gvcM9RQ)U6OdG3~vr zhOy>Kyv=yG-BXXG_Y0-S2+W=dy=Q2eS!8FV1@R57Og`M1ASeN4R54)b@6D-Wbsya? zCI^_vC}k*a`d)KY-L5pAC7(?X7_jMpG!jMphCgQZ0@)zl^D?<=tT}ad<+~e`U*(<& z$^3NkFD8k9l=-4+$_Y&-@qQt2hTgB%+kILkz5#X*ZmmYEwlsW&vi%}EA#Z*j#_LOP zYI>)tAKge4`=(ml){~RF9M@e~8J#ElLK>WI1*N9Q`Hwei8WhE@S@_{?^F{-QW4|j@ z!pg(GF(e-F8=>@y`x#I*J41RtuBxI88$=OlD&#JcXWShK9SCO(>Ik9An?A zM>mH*IVe=oV5*+*6w&E_0nFPavX8q*e)!Fag^L)1cJxQ&-B6oZ(OdWW8u#J$?c}t7 zS-9xp*eq9{wJ^5QL$2DoIO}gVo6`~;e~vG8lw&gyd&>)^4rfl3P)4lb;omi@GdHhb z86siZx_HqdrV)qhWX~$a5Z61f)C$u*$*aM@==8baINRCBtDPsUU>J=lu^E~paI}k5F~b|i zWdRhTAJTN47RwfUbfywJC;lQpfc*YAD3=44R}P>XC|whq; zzMj$!SmP5{b1M9R!Kk@HeJ0SH8gl=ksP$pEoDn-u6a%|PTq{d)KlnbWqs2g9Ng8oR znE%?!?kB@!zRqVcxBCWo?s;994}$szV*E4bO+ zm2}_g7+r~~T%S!p-Gg>U?W91BA;RhHPTOsXoPb%P7+eF)oC{^SGNiE|j@kN~?dghm zyvPVo=b|~t9BNqM5Dar=tE;Ax)ug$qT=07HauBna0I(4t${Krn`;)I$h??qJ3X(?x z5`fO;Y#m;&a%4Tq8No?trCn9Z9x7~fBoHAB4fCcQ9K#H%ES5#kM_Ym^FZ)JPxD`T; zDvt;ERVGrQ)U;@2TXWG`%%z=Fount)l;mJ0BZ;L2g3uJR+Hk@>64@f+6RWc=Rh_u7xUy1c^U!IUuynLu;QRPC$ z?NGoUB^~zbZY{HyC18a1=Iw2l-C`44xoc z+ZLHPimZXPQP)9WpnCnwXAA}~@@YqBi{kmC7A-AgcIP4UWRjbOl4M>}U6Smyfb z;^YU>=Yq}pR!hN^pN81}3FQHAu3Vd&n*rk%en*P={(~>$EFTLW{{|u)2Gu(ZNbDU9 z8_NnxaqS3tE_tRT&t)#hRLf|V^FkWjQAJYaUas>YT2N1~0e$K4iN-?0&E54j!VKYd zKSf29xoiu#vlxlI6OrG5*rhrX?%YpBf*op_tOeLG8tOzGssP(AOd1ic@-t6}_pEZi z?8w)Qyj!-;n?()N%T>#Smy$292OQh|6bPBZ7Jg!>yfPc4sW`PyCLTgB%AFq`#(v&=r&W>x`s^Ie-S3=>Cf z8p{?#1}o(8-bqgn+d)|g{rMZBKu9~(B(20bRw6uy8+~}A`T1O2EIo$3aWADv4R2H7 zI>;N}I7zqt7}9^_ly)OIxBdGR3q7ekfwZbEDC4HoHm+ET-fjc5rYX^LW79@1BJHPwf z+fsbhHz_Gaa*)X=oEc&TTL=uk4TV9Zp!~I>V`y`0yR>Gs#1v~nO1dMxB1vwap9I%~|OeB@~LxA5po)#&MalCxBwUOS= zsT(p8D;6RUE52wE1cytGMFxL&MLc^!Ha_ZQbxmw=(Ol%j5cl{P18N=i_}wBvPp(#m zq;i2(DyEcIo1NtbgUUYXfSmQ;zXc`#?%I&DpAhe^tCjx{Ia7c2HC1xMKcdL5gkgj# zbKMDI*{%(35*dxRFN}pbh$#mLE^YZw44nhIl}Jo;xBoy;#gHp4oTu?~2=tM91ORwp z1oS?ByJ|%E#ZJ^Dh_zQyOJtDl+@leOQMI8>6zeJRG8)22Hc07;ezDuM;o7v3APT12 zDAbgbR_9@@R7w0&=%q3A>9cg+xv4mAB5-wNPjzHuw>C|oF`D|bU}OcD4-%rN;7Y_5 z_-|UuNU)cQb3df2n={$8Rr>+J#_%)XE>liV!i0m&VJ5r2f;LmKxYL-fwl)u*_^Ntf z&rn3Y19O=@B)HlFx1w;nZ?1zJ^d4 zPW*F5j^9$8OV%R!>Y}-Ynn$5VoG~#~X+V2Ovq3b_(BMjB<}&Cbg>Vwvim`(P6GxA~ z3iYgHBAB_UQ>Vsyj*TdHedOE1q`f~G%$CB5RrJXxkx&be#wpjEGvkQ2_*=e4L->d3 zy4Tl|<6V8AL<&^R>%wWjNf2{I#&mI#?(Ul7g z7cOOsb3}=->n>EA?=1BqJ0dpfWon(5*Aq-GGvdm^{5O*I9d9DKGT?kaA2{loSALrV z)J8bNx!g!dSu_~N+_)#a4J8~JzHX@g_l!vW_GKGBmcC*$FHl`%p&-7E6E#`tVaXv< zFZO#s@>uQ$%U$O(RU|_h%E@*A3nMg;Wm*3mm!&ANjkVMgR}Ph&2N$78|LF^03HmO5 zqXpSWd{I-`=c+TQb^qpEF0$q3!;BMrtT(=pj%8B0QB9itqvk8hW?;XvS<_vuTv}=W zNQC)Fx7F_j#D51a zo5#ZseTMgoWm$qcu)Infp9_~G9vvTIEZ6Yc@8C5CaB`8BT5!hlHyS_vDfG$@eVeRy zpGJO`g8B~8-4X!qpn7Cf@#$2$DxgYS#$3BPGq0=Dy^GxUfwoEG4dYp7dx zcuTT#(xB+Ted`{uH9uEkk+Z3KhZB^rZc8H00jSrWmE~C)(7ibNNCV~ql^ob1&KMK> zDhN}Kr=DoxzO=bgXMLq`!WWMIYNC*luAogFXTjio;{nY*+6lY&tj3k?#QgVI+XRyT z*B}!&H=2F`wu`0oNpm?Xtf9|Q4!*s}i9Ml=5AiKE*Ck{KEILVQl_C#{0dZfWSYuO= zknzlYy&4ve3#lgRNZR9vQpD48u^{1m#KdW)L`@iEBd=OSp9@?BEgbn*dEs;zK#I(T zj3M?qy#b)I!bt-ufwIJ?)fTK*%;)2MKXICnHT~Wo0{m}dgNmS+BJ0NhZp153S~L|p zTy9`e_JBERC!>fd?kpNdGtSsj>yZWl8_7+PGUeqL5SGJVA()mdbi*8UsxFjfSq`Z` z>Q{d2c$dPxMOGPS7rquI8l&_l($&HFgwd#g?f84)d*LHZ%99sN+&;dNpzwMhwboqX zWvFk{0$-N4*I)V@g`6KmfDjooj@$Q{QFVdy{kiH^7IRf<7jto=_uYl{{iCbF95XaH z5@uNK=HiS>HAGC*Po_8wQw&XwjP@=;Eb20!JN>w^u{L0ykkz(+2y8*;q?Dtba$I>? z)oQV^IP=(I6Y_dR7?^B1WH2Fs1~7ExM-@gdVt-}?C*OYFnk{Spek&BXsQZ{|8E}6R z@_yRm$Hp9R=_@jeib^&Ua@OJ#B_})YuZFg^$||Yx$RUq4 zSPzr_J5|)f{c512GR@t~Jt))d-VnMLsGKW^E>~u<;?*1K z?`DAbsg6q?b|9pfY6$*YvzLeaG;$T+wR!xqmoD8()F)p1oVRP-Oez{Wpb$y}V`XwU7m-zJ< z*Aw>ec)fyie0@gYQw}Fo1NhzwDJFbI*EEdVF*K9K9OYmB%)!QU-yK`TK?FjkA~|y{ zXNYDnDvZo){~3Po)Mf3aW6!@j7hR~y=X;WR%1*C61c}nr<41&3RGCUo8M2N=kFQp( zSoH_vLO2<9o2T`#64~^=?SM<@p%DrnAow_pySFFwK1*Kma;`Sz=5XnuIyfR2I%Zo_ z=y0LpQ0h|HL?CpbRiMXEK91H~$pA(4=_P%eqoR;vB5X=KM8;VdrlE>t0_$Mh9n&6- zfWqk=mPamq2hEvY86=*lfCxfC#xk`Ajx`+L!#gc|QTghUK@NlIG_cY~G7z7h zRD-^2YR_ewz$}fQuBZz^3`$j}T{HIqsL&0s^o?Vfg>p!~; zp7}4TRFt1;!^{B(fjxMDn?a%X9YMa)?b#p62~m3{wheh%E$H=SXBczIo%y`$9amjOy~N~%X=FOX&? znRe7s745IAjwdQAbSiZ!Y$|0cY;tw-a<4*HjGSZowOam^^LfVC13$+LLwJB?uE&GM?k^i1 zPpHqp9b}o$;Y;C9WXC5wHlJ+UJ#=^j!PPx3f0+xc;*MK%afmDc9;L5&flL46$-s z)|&e3lSD&3pH+J4^^p=DcMoU>Nps1{NtQRIDruH^ClcQiHs?fS)ybA0daOz;%`Mt3*ss4)gjV7DmY;5(AKtVTp$EeGm`0kv zA|I5&beAF3834gV;;MFs4+=#(_a80ipvcc zB!~ZY_L3WT&kHA;Nz9iKQ@#Mj*ns!*Zf)C|-X|EKW*KMU9_<>*fWRzF$ilN|Qq2dL zI@2in<_8?TL%w;sMH&oRkV@#2v!cXX=y88jJdALn8&20|MoGs$lr*r>ic`lZ-biB3 za254m0qV~r>aTL0DH1KIJh~qt*b{5FWof~8`m^{DT0(rhAs{F_ltdWc*XUOP!;lvW zGSTx@!@j9$l{@;*FD7EykIt`K(DIg)j+_8$t`K3KT(*KuAW4CK9jZ`q7Tih*l?Y-e$QB zsMR1Yalc5J-M-geE&VIdrMqDzL0vP0Lm2cfcG&Aj2tBnNkNTUEOS{3w|gy%RcH&*F^O1ENgQ{B7T1 z_-wdRL2#gg1(HQ-)W|76@0*ASFYjfs{9{(?C(IxU%7!gt-%D+=67H9+g>2E)z_zaQ zRrfAOnrKx-6Czc}Jse?VLu~?&<>>7Jq`-Ko!d#w09vIc00|BUuY^CO%M0kMY@>+=9 zZ9Hk^l(#48>%9=b+oqlzvOxr6pTUx}HSjiF?5_sZXlJ%US;6;PS4)yfSnYQ<+x9BK zi0UC_N~%R-2QhcH+T@DNqzl^mK_>8DG7ez$aGwPdQFC7z%y%Xw=lYlob(aT=ZfKd~ zjPv@-lE!-%s}{8eX5uMNF2eg$hpqm#nfK@a;1ue=1)t0p{H#G;jWrCwtQXFotUWmN zEz|H~|IWt*TT36K=9wQ{HW3Hq6RmxXa*P&2qjXr638@L7a;P)$U*6@l{@jb8{UzwMVtT(Sx0*KFrk;bhKf z3J!?qvRp}(3rr9Mp;@GvrsG*HPV~MCa$yVNly7_h1>NG4aU;oHAI9&E{-%~*&N2vv z!tW7BvWadaAy-I7s+4g!$=u<_AJx$x$Y_^&K8s6LIA)Cmif7AxF*nJwo8e=%v2O$HWmj8~(PZ{k3akK+b#D(Ya#Bw>) zjE~&gWS1I?y)%K9<#xZN3R0i=a{?m513} z`6QX`2;<;d@O-+Wq-{W41m133-{OO9AzpFFEs5IV>J>5thsPy_? zz&!q~Jwt`$6MSc)g6Uk&$bI%!{g#SUtfIu5IGd(W9btq*;l&~l@BLEFf$04o(pzsd3H@~+bs0P9U$1**4LFXDgYRvlHv_S| zn8^3y4RwFkYdxoFttb(P1uM8VZ}hCJj=(GYs!1VAzA>WR)gVpL10QNe{z;)RQJmYhL#E9S=-Ym7$q$m_&8sH0;LXc+v2+V?6H_T_%~-eHQe; z0nM<5E?kRS)$}W(hrsd;36FYc6MLbHk~Sg4-9T{eec|!$8y~XZXRD0c9S$c)REt83 z`=8CkhAnU_`)=ka89QZ`v6jUo+XETLQ`IVubT{g(m^5ms^1irz?w7* Date: Wed, 6 Sep 2017 19:19:59 +0800 Subject: [PATCH 02/38] missing '/' in doc --- docs/sanic/routing.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sanic/routing.md b/docs/sanic/routing.md index 7a562b66..98179e17 100644 --- a/docs/sanic/routing.md +++ b/docs/sanic/routing.md @@ -323,7 +323,7 @@ app.blueprint(bp) # then build the url app.url_for('static', filename='file.txt') == '/static/file.txt' app.url_for('static', name='static', filename='file.txt') == '/static/file.txt' -app.url_for('static', name='uploads', filename='file.txt') == 'uploads/file.txt' +app.url_for('static', name='uploads', filename='file.txt') == '/uploads/file.txt' app.url_for('static', name='best_png') == '/the_best.png' # blueprint url building From c9a40c180a60f7c6f4943c619d81d71093e703c1 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Sun, 10 Sep 2017 11:11:16 -0700 Subject: [PATCH 03/38] remove some logging stuff --- sanic/app.py | 18 ++--------- sanic/config.py | 83 ------------------------------------------------- sanic/log.py | 13 -------- 3 files changed, 3 insertions(+), 111 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 20c02a5c..c585bf43 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -10,7 +10,7 @@ from traceback import format_exc from urllib.parse import urlencode, urlunparse from ssl import create_default_context, Purpose -from sanic.config import Config, LOGGING +from sanic.config import Config from sanic.constants import HTTP_METHODS from sanic.exceptions import ServerError, URLBuildError, SanicException from sanic.handlers import ErrorHandler @@ -28,18 +28,7 @@ class Sanic: def __init__(self, name=None, router=None, error_handler=None, load_env=True, request_class=None, - log_config=LOGGING, strict_slashes=False): - if log_config: - logging.config.dictConfig(log_config) - # Only set up a default log handler if the - # end-user application didn't set anything up. - if not logging.root.handlers and log.level == logging.NOTSET: - formatter = logging.Formatter( - "%(asctime)s: %(levelname)s: %(message)s") - handler = logging.StreamHandler() - handler.setFormatter(formatter) - log.addHandler(handler) - log.setLevel(logging.INFO) + strict_slashes=False): # Get name from previous stack frame if name is None: @@ -51,7 +40,6 @@ class Sanic: self.request_class = request_class self.error_handler = error_handler or ErrorHandler() self.config = Config(load_env=load_env) - self.log_config = log_config self.request_middleware = deque() self.response_middleware = deque() self.blueprints = {} @@ -642,7 +630,7 @@ class Sanic: async def create_server(self, host=None, port=None, debug=False, ssl=None, sock=None, protocol=None, backlog=100, stop_event=None, - log_config=LOGGING): + log_config=None): """Asynchronous version of `run`. NOTE: This does not support multiprocessing and is not the preferred diff --git a/sanic/config.py b/sanic/config.py index 0c2cc701..716ec1a6 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -15,89 +15,6 @@ _address_dict = { 'FreeBSD': '/var/run/log' } -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'filters': { - 'accessFilter': { - '()': DefaultFilter, - 'param': [0, 10, 20] - }, - 'errorFilter': { - '()': DefaultFilter, - 'param': [30, 40, 50] - } - }, - 'formatters': { - 'simple': { - 'format': '%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s', - 'datefmt': '%Y-%m-%d %H:%M:%S' - }, - 'access': { - 'format': '%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: ' + - '%(request)s %(message)s %(status)d %(byte)d', - 'datefmt': '%Y-%m-%d %H:%M:%S' - } - }, - 'handlers': { - 'internal': { - 'class': 'logging.StreamHandler', - 'filters': ['accessFilter'], - 'formatter': 'simple', - 'stream': sys.stderr - }, - 'accessStream': { - 'class': 'logging.StreamHandler', - 'filters': ['accessFilter'], - 'formatter': 'access', - 'stream': sys.stderr - }, - 'errorStream': { - 'class': 'logging.StreamHandler', - 'filters': ['errorFilter'], - 'formatter': 'simple', - 'stream': sys.stderr - }, - # before you use accessSysLog, be sure that log levels - # 0, 10, 20 have been enabled in you syslog configuration - # otherwise you won't be able to see the output in syslog - # logging file. - 'accessSysLog': { - 'class': 'logging.handlers.SysLogHandler', - 'address': _address_dict.get(platform.system(), - ('localhost', 514)), - 'facility': syslog.LOG_DAEMON, - 'filters': ['accessFilter'], - 'formatter': 'access' - }, - 'errorSysLog': { - 'class': 'logging.handlers.SysLogHandler', - 'address': _address_dict.get(platform.system(), - ('localhost', 514)), - 'facility': syslog.LOG_DAEMON, - 'filters': ['errorFilter'], - 'formatter': 'simple' - }, - }, - 'loggers': { - 'sanic': { - 'level': 'INFO', - 'handlers': ['internal', 'errorStream'] - }, - 'network': { - 'level': 'INFO', - 'handlers': ['accessStream', 'errorStream'] - } - } -} - -# this happens when using container or systems without syslog -# keep things in config would cause file not exists error -_addr = LOGGING['handlers']['accessSysLog']['address'] -if type(_addr) is str and not os.path.exists(_addr): - LOGGING['handlers'].pop('accessSysLog') - LOGGING['handlers'].pop('errorSysLog') - class Config(dict): def __init__(self, defaults=None, load_env=True, keep_alive=True): diff --git a/sanic/log.py b/sanic/log.py index 760ad1c6..a7933c0d 100644 --- a/sanic/log.py +++ b/sanic/log.py @@ -1,18 +1,5 @@ import logging -class DefaultFilter(logging.Filter): - - def __init__(self, param=None): - self.param = param - - def filter(self, record): - if self.param is None: - return True - if record.levelno in self.param: - return True - return False - - log = logging.getLogger('sanic') netlog = logging.getLogger('network') From c9cbc00e362eda60f3af296d743e1ee6ca69626f Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Sun, 10 Sep 2017 18:38:52 -0700 Subject: [PATCH 04/38] use access_log as param --- sanic/app.py | 17 ++++++----------- sanic/server.py | 20 ++++++++++---------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index c585bf43..595b50c1 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -567,7 +567,7 @@ class Sanic: def run(self, host=None, port=None, debug=False, ssl=None, sock=None, workers=1, protocol=None, backlog=100, stop_event=None, register_sys_signals=True, - log_config=None): + access_log=True): """Run the HTTP Server and listen until keyboard interrupt or term signal. On termination, drain connections before closing. @@ -588,9 +588,6 @@ class Sanic: if sock is None: host, port = host or "127.0.0.1", port or 8000 - if log_config: - self.log_config = log_config - logging.config.dictConfig(log_config) if protocol is None: protocol = (WebSocketProtocol if self.websocket_enabled else HttpProtocol) @@ -603,7 +600,7 @@ class Sanic: host=host, port=port, debug=debug, ssl=ssl, sock=sock, workers=workers, protocol=protocol, backlog=backlog, register_sys_signals=register_sys_signals, - has_log=self.log_config is not None) + access_log=access_log) try: self.is_running = True @@ -630,7 +627,7 @@ class Sanic: async def create_server(self, host=None, port=None, debug=False, ssl=None, sock=None, protocol=None, backlog=100, stop_event=None, - log_config=None): + access_log=True): """Asynchronous version of `run`. NOTE: This does not support multiprocessing and is not the preferred @@ -639,8 +636,6 @@ class Sanic: if sock is None: host, port = host or "127.0.0.1", port or 8000 - if log_config: - logging.config.dictConfig(log_config) if protocol is None: protocol = (WebSocketProtocol if self.websocket_enabled else HttpProtocol) @@ -654,7 +649,7 @@ class Sanic: host=host, port=port, debug=debug, ssl=ssl, sock=sock, loop=get_event_loop(), protocol=protocol, backlog=backlog, run_async=True, - has_log=log_config is not None) + access_log=access_log) # Trigger before_start events await self.trigger_events( @@ -699,7 +694,7 @@ class Sanic: def _helper(self, host=None, port=None, debug=False, ssl=None, sock=None, workers=1, loop=None, protocol=HttpProtocol, backlog=100, stop_event=None, - register_sys_signals=True, run_async=False, has_log=True): + register_sys_signals=True, run_async=False, access_log=True): """Helper function used by `run` and `create_server`.""" if isinstance(ssl, dict): # try common aliaseses @@ -738,7 +733,7 @@ class Sanic: 'loop': loop, 'register_sys_signals': register_sys_signals, 'backlog': backlog, - 'has_log': has_log, + 'access_log': access_log, 'websocket_max_size': self.config.WEBSOCKET_MAX_SIZE, 'websocket_max_queue': self.config.WEBSOCKET_MAX_QUEUE, 'graceful_shutdown_timeout': self.config.GRACEFUL_SHUTDOWN_TIMEOUT diff --git a/sanic/server.py b/sanic/server.py index f62ba654..3e52d634 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -65,15 +65,15 @@ class HttpProtocol(asyncio.Protocol): # request config 'request_handler', 'request_timeout', 'request_max_size', 'request_class', 'is_request_stream', 'router', - # enable or disable access log / error log purpose - 'has_log', + # enable or disable access log purpose + 'access_log', # connection management '_total_request_size', '_timeout_handler', '_last_communication_time', '_is_stream_handler') def __init__(self, *, loop, request_handler, error_handler, signal=Signal(), connections=set(), request_timeout=60, - request_max_size=None, request_class=None, has_log=True, + request_max_size=None, request_class=None, access_log=True, keep_alive=True, is_request_stream=False, router=None, state=None, debug=False, **kwargs): self.loop = loop @@ -84,7 +84,7 @@ class HttpProtocol(asyncio.Protocol): self.headers = None self.router = router self.signal = signal - self.has_log = has_log + self.access_log = access_log self.connections = connections self.request_handler = request_handler self.error_handler = error_handler @@ -246,7 +246,7 @@ class HttpProtocol(asyncio.Protocol): response.output( self.request.version, keep_alive, self.request_timeout)) - if self.has_log: + if self.access_log: netlog.info('', extra={ 'status': response.status, 'byte': len(response.body), @@ -288,7 +288,7 @@ class HttpProtocol(asyncio.Protocol): response.transport = self.transport await response.stream( self.request.version, keep_alive, self.request_timeout) - if self.has_log: + if self.access_log: netlog.info('', extra={ 'status': response.status, 'byte': -1, @@ -333,7 +333,7 @@ class HttpProtocol(asyncio.Protocol): "Writing error failed, connection closed {}".format(repr(e)), from_error=True) finally: - if self.has_log: + if self.access_log: extra = dict() if isinstance(response, HTTPResponse): extra['status'] = response.status @@ -424,7 +424,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, request_timeout=60, ssl=None, sock=None, request_max_size=None, reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100, register_sys_signals=True, run_async=False, connections=None, - signal=Signal(), request_class=None, has_log=True, keep_alive=True, + signal=Signal(), request_class=None, access_log=True, keep_alive=True, is_request_stream=False, router=None, websocket_max_size=None, websocket_max_queue=None, state=None, graceful_shutdown_timeout=15.0): @@ -453,7 +453,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, :param loop: asyncio compatible event loop :param protocol: subclass of asyncio protocol class :param request_class: Request class to use - :param has_log: disable/enable access log and error log + :param access_log: disable/enable access log :param is_request_stream: disable/enable Request.stream :param router: Router object :return: Nothing @@ -476,7 +476,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, request_timeout=request_timeout, request_max_size=request_max_size, request_class=request_class, - has_log=has_log, + access_log=access_log, keep_alive=keep_alive, is_request_stream=is_request_stream, router=router, From 986135ff76f0a4e94d0842e5d1c79c5c43491e2f Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Sun, 10 Sep 2017 18:39:42 -0700 Subject: [PATCH 05/38] remove DefaultFilter --- sanic/config.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/sanic/config.py b/sanic/config.py index 716ec1a6..1f0bbd3e 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -4,8 +4,6 @@ import syslog import platform import types -from sanic.log import DefaultFilter - SANIC_PREFIX = 'SANIC_' _address_dict = { From 8f6fa5e9ffe0580407c8a999e35171664b4010b8 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Sun, 10 Sep 2017 18:44:54 -0700 Subject: [PATCH 06/38] old logging cleanup --- tests/test_logging.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/tests/test_logging.py b/tests/test_logging.py index d6911d86..3d75dbe0 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -1,7 +1,6 @@ import uuid from importlib import reload -from sanic.config import LOGGING from sanic.response import text from sanic import Sanic from io import StringIO @@ -38,20 +37,3 @@ def test_log(): request, response = app.test_client.get('/') log_text = log_stream.getvalue() assert rand_string in log_text - - -def test_default_log_fmt(): - - reset_logging() - Sanic() - for fmt in [h.formatter for h in logging.getLogger('sanic').handlers]: - assert fmt._fmt == LOGGING['formatters']['simple']['format'] - - reset_logging() - Sanic(log_config=None) - for fmt in [h.formatter for h in logging.getLogger('sanic').handlers]: - assert fmt._fmt == "%(asctime)s: %(levelname)s: %(message)s" - - -if __name__ == "__main__": - test_log() From 4bdb9a2c8e0d323352e136624e9558c4d1633aa9 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Sun, 10 Sep 2017 23:19:09 -0700 Subject: [PATCH 07/38] prototype --- sanic/log.py | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 52 insertions(+), 1 deletion(-) diff --git a/sanic/log.py b/sanic/log.py index a7933c0d..3d254d6c 100644 --- a/sanic/log.py +++ b/sanic/log.py @@ -1,5 +1,56 @@ import logging +LOGGING_CONFIG_DEFAULTS = dict( + version=1, + disable_existing_loggers=False, + + loggers={ + "root": { + "level": "INFO", + "handlers": ["console"]}, + "sanic.error": { + "level": "INFO", + "handlers": ["error_console"], + "propagate": True, + "qualname": "sanic.error" + }, + + "sanic.access": { + "level": "INFO", + "handlers": ["console"], + "propagate": True, + "qualname": "sanic.access" + } + }, + handlers={ + "console": { + "class": "logging.StreamHandler", + "formatter": "generic", + "stream": "sys.stdout" + }, + "error_console": { + "class": "logging.StreamHandler", + "formatter": "generic", + "stream": "sys.stderr" + }, + }, + formatters={ + "generic": { + "format": "%(asctime)s [%(process)d] [%(levelname)s] %(message)s", + "datefmt": "[%Y-%m-%d %H:%M:%S %z]", + "class": "logging.Formatter" + } + } +) + + +class AccessLogger: + + def __init__(self, logger, access_log_format=None): + pass + + log = logging.getLogger('sanic') -netlog = logging.getLogger('network') +error_logger = logging.getLogger('sanic.error') +access_logger = logging.getLogger('sanic.access') From 2979e03148540d4d16e797ebb1999da58843d87d Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Mon, 11 Sep 2017 17:17:33 +1000 Subject: [PATCH 08/38] WIP - Split RequestTimeout, ResponseTimout, and KeepAliveTimeout into different timeouts, with different callbacks. --- sanic/app.py | 2 + sanic/config.py | 8 ++ sanic/exceptions.py | 14 +++ sanic/server.py | 136 +++++++++++++++++++++++------ tests/test_keep_alive_timeout.py | 142 +++++++++++++++++++++++++++++++ tests/test_request_timeout.py | 101 +++++++++++++++++----- tests/test_response_timeout.py | 38 +++++++++ 7 files changed, 395 insertions(+), 46 deletions(-) create mode 100644 tests/test_keep_alive_timeout.py create mode 100644 tests/test_response_timeout.py diff --git a/sanic/app.py b/sanic/app.py index 20c02a5c..4a2ea01c 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -745,6 +745,8 @@ class Sanic: 'request_handler': self.handle_request, 'error_handler': self.error_handler, 'request_timeout': self.config.REQUEST_TIMEOUT, + 'response_timeout': self.config.RESPONSE_TIMEOUT, + 'keep_alive_timeout': self.config.KEEP_ALIVE_TIMEOUT, 'request_max_size': self.config.REQUEST_MAX_SIZE, 'keep_alive': self.config.KEEP_ALIVE, 'loop': loop, diff --git a/sanic/config.py b/sanic/config.py index 0c2cc701..560fa2ec 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -125,7 +125,15 @@ class Config(dict): """ self.REQUEST_MAX_SIZE = 100000000 # 100 megabytes self.REQUEST_TIMEOUT = 60 # 60 seconds + self.RESPONSE_TIMEOUT = 60 # 60 seconds self.KEEP_ALIVE = keep_alive + # Apache httpd server default keepalive timeout = 5 seconds + # Nginx server default keepalive timeout = 75 seconds + # Nginx performance tuning guidelines uses keepalive timeout = 15 seconds + # IE client hard keepalive limit = 60 seconds + # Firefox client hard keepalive limit = 115 seconds + + self.KEEP_ALIVE_TIMEOUT = 5 # 5 seconds self.WEBSOCKET_MAX_SIZE = 2 ** 20 # 1 megabytes self.WEBSOCKET_MAX_QUEUE = 32 self.GRACEFUL_SHUTDOWN_TIMEOUT = 15.0 # 15 sec diff --git a/sanic/exceptions.py b/sanic/exceptions.py index 9663ea7c..e2d808f7 100644 --- a/sanic/exceptions.py +++ b/sanic/exceptions.py @@ -155,6 +155,13 @@ class ServerError(SanicException): pass +@add_status_code(503) +class ServiceUnavailable(SanicException): + """The server is currently unavailable (because it is overloaded or + down for maintenance). Generally, this is a temporary state.""" + pass + + class URLBuildError(ServerError): pass @@ -170,6 +177,13 @@ class FileNotFound(NotFound): @add_status_code(408) class RequestTimeout(SanicException): + """The Web server (running the Web site) thinks that there has been too + long an interval of time between 1) the establishment of an IP + connection (socket) between the client and the server and + 2) the receipt of any data on that socket, so the server has dropped + the connection. The socket connection has actually been lost - the Web + server has 'timed out' on that particular socket connection. + """ pass diff --git a/sanic/server.py b/sanic/server.py index f62ba654..bcef8a91 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -28,7 +28,8 @@ from sanic.log import log, netlog from sanic.response import HTTPResponse from sanic.request import Request from sanic.exceptions import ( - RequestTimeout, PayloadTooLarge, InvalidUsage, ServerError) + RequestTimeout, PayloadTooLarge, InvalidUsage, ServerError, + ServiceUnavailable) current_time = None @@ -63,16 +64,19 @@ class HttpProtocol(asyncio.Protocol): # request params 'parser', 'request', 'url', 'headers', # request config - 'request_handler', 'request_timeout', 'request_max_size', - 'request_class', 'is_request_stream', 'router', + 'request_handler', 'request_timeout', 'response_timeout', + 'keep_alive_timeout', 'request_max_size', 'request_class', + 'is_request_stream', 'router', # enable or disable access log / error log purpose 'has_log', # connection management - '_total_request_size', '_timeout_handler', '_last_communication_time', - '_is_stream_handler') + '_total_request_size', '_request_timeout_handler', + '_response_timeout_handler', '_keep_alive_timeout_handler', + '_last_request_time', '_last_response_time', '_is_stream_handler') def __init__(self, *, loop, request_handler, error_handler, signal=Signal(), connections=set(), request_timeout=60, + response_timeout=60, keep_alive_timeout=15, request_max_size=None, request_class=None, has_log=True, keep_alive=True, is_request_stream=False, router=None, state=None, debug=False, **kwargs): @@ -89,13 +93,18 @@ class HttpProtocol(asyncio.Protocol): self.request_handler = request_handler self.error_handler = error_handler self.request_timeout = request_timeout + self.response_timeout = response_timeout + self.keep_alive_timeout = keep_alive_timeout self.request_max_size = request_max_size self.request_class = request_class or Request self.is_request_stream = is_request_stream self._is_stream_handler = False self._total_request_size = 0 - self._timeout_handler = None + self._request_timeout_handler = None + self._response_timeout_handler = None + self._keep_alive_timeout_handler = None self._last_request_time = None + self._last_response_time = None self._request_handler_task = None self._request_stream_task = None self._keep_alive = keep_alive @@ -118,22 +127,32 @@ class HttpProtocol(asyncio.Protocol): def connection_made(self, transport): self.connections.add(self) - self._timeout_handler = self.loop.call_later( - self.request_timeout, self.connection_timeout) + self._request_timeout_handler = self.loop.call_later( + self.request_timeout, self.request_timeout_callback) self.transport = transport self._last_request_time = current_time def connection_lost(self, exc): self.connections.discard(self) - self._timeout_handler.cancel() + if self._request_timeout_handler: + self._request_timeout_handler.cancel() + if self._response_timeout_handler: + self._response_timeout_handler.cancel() + if self._keep_alive_timeout_handler: + self._keep_alive_timeout_handler.cancel() - def connection_timeout(self): - # Check if + def request_timeout_callback(self): + # See the docstring in the RequestTimeout exception, to see + # exactly what this timeout is checking for. + # Check if elapsed time since request initiated exceeds our + # configured maximum request timeout value time_elapsed = current_time - self._last_request_time if time_elapsed < self.request_timeout: time_left = self.request_timeout - time_elapsed - self._timeout_handler = ( - self.loop.call_later(time_left, self.connection_timeout)) + self._request_timeout_handler = ( + self.loop.call_later(time_left, + self.request_timeout_callback) + ) else: if self._request_stream_task: self._request_stream_task.cancel() @@ -144,6 +163,37 @@ class HttpProtocol(asyncio.Protocol): except RequestTimeout as exception: self.write_error(exception) + def response_timeout_callback(self): + # Check if elapsed time since response was initiated exceeds our + # configured maximum request timeout value + time_elapsed = current_time - self._last_request_time + if time_elapsed < self.response_timeout: + time_left = self.response_timeout - time_elapsed + self._response_timeout_handler = ( + self.loop.call_later(time_left, + self.response_timeout_callback) + ) + else: + try: + raise ServiceUnavailable('Response Timeout') + except ServiceUnavailable as exception: + self.write_error(exception) + + def keep_alive_timeout_callback(self): + # Check if elapsed time since last response exceeds our configured + # maximum keep alive timeout value + time_elapsed = current_time - self._last_response_time + if time_elapsed < self.keep_alive_timeout: + time_left = self.keep_alive_timeout - time_elapsed + self._keep_alive_timeout_handler = ( + self.loop.call_later(time_left, + self.keep_alive_timeout_callback) + ) + else: + log.info('KeepAlive Timeout. Closing connection.') + self.transport.close() + + # -------------------------------------------- # # Parsing # -------------------------------------------- # @@ -204,6 +254,11 @@ class HttpProtocol(asyncio.Protocol): method=self.parser.get_method().decode(), transport=self.transport ) + # Remove any existing KeepAlive handler here, + # It will be recreated if required on the new request. + if self._keep_alive_timeout_handler: + self._keep_alive_timeout_handler.cancel() + self._keep_alive_timeout_handler = None if self.is_request_stream: self._is_stream_handler = self.router.is_stream_handler( self.request) @@ -219,6 +274,11 @@ class HttpProtocol(asyncio.Protocol): self.request.body.append(body) def on_message_complete(self): + # Entire request (headers and whole body) is received. + # We can cancel and remove the request timeout handler now. + if self._request_timeout_handler: + self._request_timeout_handler.cancel() + self._request_timeout_handler = None if self.is_request_stream and self._is_stream_handler: self._request_stream_task = self.loop.create_task( self.request.stream.put(None)) @@ -227,6 +287,9 @@ class HttpProtocol(asyncio.Protocol): self.execute_request_handler() def execute_request_handler(self): + self._response_timeout_handler = self.loop.call_later( + self.response_timeout, self.response_timeout_callback) + self._last_request_time = current_time self._request_handler_task = self.loop.create_task( self.request_handler( self.request, @@ -240,12 +303,15 @@ class HttpProtocol(asyncio.Protocol): """ Writes response content synchronously to the transport. """ + if self._response_timeout_handler: + self._response_timeout_handler.cancel() + self._response_timeout_handler = None try: keep_alive = self.keep_alive self.transport.write( response.output( self.request.version, keep_alive, - self.request_timeout)) + self.keep_alive_timeout)) if self.has_log: netlog.info('', extra={ 'status': response.status, @@ -273,7 +339,10 @@ class HttpProtocol(asyncio.Protocol): if not keep_alive: self.transport.close() else: - self._last_request_time = current_time + self._keep_alive_timeout_handler = self.loop.call_later( + self.keep_alive_timeout, + self.keep_alive_timeout_callback) + self._last_response_time = current_time self.cleanup() async def stream_response(self, response): @@ -282,12 +351,14 @@ class HttpProtocol(asyncio.Protocol): the transport to the response so the response consumer can write to the response as needed. """ - + if self._response_timeout_handler: + self._response_timeout_handler.cancel() + self._response_timeout_handler = None try: keep_alive = self.keep_alive response.transport = self.transport await response.stream( - self.request.version, keep_alive, self.request_timeout) + self.request.version, keep_alive, self.keep_alive_timeout) if self.has_log: netlog.info('', extra={ 'status': response.status, @@ -315,10 +386,18 @@ class HttpProtocol(asyncio.Protocol): if not keep_alive: self.transport.close() else: - self._last_request_time = current_time + self._keep_alive_timeout_handler = self.loop.call_later( + self.keep_alive_timeout, + self.keep_alive_timeout_callback) + self._last_response_time = current_time self.cleanup() def write_error(self, exception): + # An error _is_ a response. + # Don't throw a response timeout, when a response _is_ given. + if self._response_timeout_handler: + self._response_timeout_handler.cancel() + self._response_timeout_handler = None response = None try: response = self.error_handler.response(self.request, exception) @@ -330,8 +409,9 @@ class HttpProtocol(asyncio.Protocol): self.request.ip if self.request else 'Unknown')) except Exception as e: self.bail_out( - "Writing error failed, connection closed {}".format(repr(e)), - from_error=True) + "Writing error failed, connection closed {}".format( + repr(e)), from_error=True + ) finally: if self.has_log: extra = dict() @@ -367,6 +447,9 @@ class HttpProtocol(asyncio.Protocol): log.error(message) def cleanup(self): + """This is called when KeepAlive feature is used, + it resets the connection in order for it to be able + to handle receiving another request on the same connection.""" self.parser = None self.request = None self.url = None @@ -421,12 +504,13 @@ def trigger_events(events, loop): def serve(host, port, request_handler, error_handler, before_start=None, after_start=None, before_stop=None, after_stop=None, debug=False, - request_timeout=60, ssl=None, sock=None, request_max_size=None, - reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100, + request_timeout=60, response_timeout=60, keep_alive_timeout=60, + ssl=None, sock=None, request_max_size=None, reuse_port=False, + loop=None, protocol=HttpProtocol, backlog=100, register_sys_signals=True, run_async=False, connections=None, - signal=Signal(), request_class=None, has_log=True, keep_alive=True, - is_request_stream=False, router=None, websocket_max_size=None, - websocket_max_queue=None, state=None, + signal=Signal(), request_class=None, has_log=True, + keep_alive=True, is_request_stream=False, router=None, + websocket_max_size=None, websocket_max_queue=None, state=None, graceful_shutdown_timeout=15.0): """Start asynchronous HTTP Server on an individual process. @@ -474,6 +558,8 @@ def serve(host, port, request_handler, error_handler, before_start=None, request_handler=request_handler, error_handler=error_handler, request_timeout=request_timeout, + response_timeout=response_timeout, + keep_alive_timeout=keep_alive_timeout, request_max_size=request_max_size, request_class=request_class, has_log=has_log, diff --git a/tests/test_keep_alive_timeout.py b/tests/test_keep_alive_timeout.py new file mode 100644 index 00000000..28030144 --- /dev/null +++ b/tests/test_keep_alive_timeout.py @@ -0,0 +1,142 @@ +from json import JSONDecodeError +from sanic import Sanic +from time import sleep as sync_sleep +import asyncio +from sanic.response import text +from sanic.config import Config +import aiohttp +from aiohttp import TCPConnector +from sanic.testing import SanicTestClient, HOST, PORT + + +class ReuseableTCPConnector(TCPConnector): + def __init__(self, *args, **kwargs): + super(ReuseableTCPConnector, self).__init__(*args, **kwargs) + self.conn = None + + @asyncio.coroutine + def connect(self, req): + if self.conn: + return self.conn + conn = yield from super(ReuseableTCPConnector, self).connect(req) + self.conn = conn + return conn + + def close(self): + return super(ReuseableTCPConnector, self).close() + + +class ReuseableSanicTestClient(SanicTestClient): + def __init__(self, app): + super(ReuseableSanicTestClient, self).__init__(app) + self._tcp_connector = None + self._session = None + + def _sanic_endpoint_test( + self, method='get', uri='/', gather_request=True, + debug=False, server_kwargs={}, + *request_args, **request_kwargs): + results = [None, None] + exceptions = [] + + if gather_request: + def _collect_request(request): + if results[0] is None: + results[0] = request + + self.app.request_middleware.appendleft(_collect_request) + + @self.app.listener('after_server_start') + async def _collect_response(sanic, loop): + try: + response = await self._local_request( + method, uri, *request_args, + **request_kwargs) + results[-1] = response + except Exception as e: + log.error( + 'Exception:\n{}'.format(traceback.format_exc())) + exceptions.append(e) + self.app.stop() + + server = self.app.create_server(host=HOST, debug=debug, port=PORT, **server_kwargs) + self.app.listeners['after_server_start'].pop() + + if exceptions: + raise ValueError( + "Exception during request: {}".format(exceptions)) + + if gather_request: + try: + request, response = results + return request, response + except: + raise ValueError( + "Request and response object expected, got ({})".format( + results)) + else: + try: + return results[-1] + except: + raise ValueError( + "Request object expected, got ({})".format(results)) + + async def _local_request(self, method, uri, cookies=None, *args, + **kwargs): + if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')): + url = uri + else: + url = 'http://{host}:{port}{uri}'.format( + host=HOST, port=PORT, uri=uri) + if self._session: + session = self._session + else: + if self._tcp_connector: + conn = self._tcp_connector + else: + conn = ReuseableTCPConnector(verify_ssl=False) + self._tcp_connector = conn + session = aiohttp.ClientSession(cookies=cookies, + connector=conn) + self._session = session + + async with getattr(session, method.lower())( + url, *args, **kwargs) as response: + try: + response.text = await response.text() + except UnicodeDecodeError: + response.text = None + + try: + response.json = await response.json() + except (JSONDecodeError, + UnicodeDecodeError, + aiohttp.ClientResponseError): + response.json = None + + response.body = await response.read() + return response + + +Config.KEEP_ALIVE_TIMEOUT = 30 +Config.KEEP_ALIVE = True +keep_alive_timeout_app = Sanic('test_request_timeout') + + +@keep_alive_timeout_app.route('/1') +async def handler(request): + return text('OK') + + +def test_keep_alive_timeout(): + client = ReuseableSanicTestClient(keep_alive_timeout_app) + headers = { + 'Connection': 'keep-alive' + } + request, response = client.get('/1', headers=headers) + assert response.status == 200 + #sync_sleep(2) + request, response = client.get('/1') + assert response.status == 200 + + diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py index 404aec12..e6c1f657 100644 --- a/tests/test_request_timeout.py +++ b/tests/test_request_timeout.py @@ -1,38 +1,97 @@ +from json import JSONDecodeError from sanic import Sanic import asyncio from sanic.response import text from sanic.exceptions import RequestTimeout from sanic.config import Config +import aiohttp +from aiohttp import TCPConnector +from sanic.testing import SanicTestClient, HOST, PORT + + +class DelayableTCPConnector(TCPConnector): + class DelayableHttpRequest(object): + def __new__(cls, req, delay): + cls = super(DelayableTCPConnector.DelayableHttpRequest, cls).\ + __new__(cls) + cls.req = req + cls.delay = delay + return cls + + def __getattr__(self, item): + return getattr(self.req, item) + + def send(self, *args, **kwargs): + if self.delay and self.delay > 0: + _ = yield from asyncio.sleep(self.delay) + self.req.send(*args, **kwargs) + + def __init__(self, *args, **kwargs): + _post_connect_delay = kwargs.pop('post_connect_delay', 0) + _pre_request_delay = kwargs.pop('pre_request_delay', 0) + super(DelayableTCPConnector, self).__init__(*args, **kwargs) + self._post_connect_delay = _post_connect_delay + self._pre_request_delay = _pre_request_delay + + @asyncio.coroutine + def connect(self, req): + req = DelayableTCPConnector.\ + DelayableHttpRequest(req, self._pre_request_delay) + conn = yield from super(DelayableTCPConnector, self).connect(req) + if self._post_connect_delay and self._post_connect_delay > 0: + _ = yield from asyncio.sleep(self._post_connect_delay) + return conn + + +class DelayableSanicTestClient(SanicTestClient): + def __init__(self, app, request_delay=1): + super(DelayableSanicTestClient, self).__init__(app) + self._request_delay = request_delay + + async def _local_request(self, method, uri, cookies=None, *args, + **kwargs): + if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')): + url = uri + else: + url = 'http://{host}:{port}{uri}'.format( + host=HOST, port=PORT, uri=uri) + + conn = DelayableTCPConnector(pre_request_delay=self._request_delay, + verify_ssl=False) + async with aiohttp.ClientSession( + cookies=cookies, connector=conn) as session: + # Insert a delay after creating the connection + # But before sending the request. + + async with getattr(session, method.lower())( + url, *args, **kwargs) as response: + try: + response.text = await response.text() + except UnicodeDecodeError: + response.text = None + + try: + response.json = await response.json() + except (JSONDecodeError, + UnicodeDecodeError, + aiohttp.ClientResponseError): + response.json = None + + response.body = await response.read() + return response + Config.REQUEST_TIMEOUT = 1 -request_timeout_app = Sanic('test_request_timeout') request_timeout_default_app = Sanic('test_request_timeout_default') -@request_timeout_app.route('/1') -async def handler_1(request): - await asyncio.sleep(2) - return text('OK') - - -@request_timeout_app.exception(RequestTimeout) -def handler_exception(request, exception): - return text('Request Timeout from error_handler.', 408) - - -def test_server_error_request_timeout(): - request, response = request_timeout_app.test_client.get('/1') - assert response.status == 408 - assert response.text == 'Request Timeout from error_handler.' - - @request_timeout_default_app.route('/1') -async def handler_2(request): - await asyncio.sleep(2) +async def handler(request): return text('OK') def test_default_server_error_request_timeout(): - request, response = request_timeout_default_app.test_client.get('/1') + client = DelayableSanicTestClient(request_timeout_default_app, 2) + request, response = client.get('/1') assert response.status == 408 assert response.text == 'Error: Request Timeout' diff --git a/tests/test_response_timeout.py b/tests/test_response_timeout.py new file mode 100644 index 00000000..bf55a42e --- /dev/null +++ b/tests/test_response_timeout.py @@ -0,0 +1,38 @@ +from sanic import Sanic +import asyncio +from sanic.response import text +from sanic.exceptions import ServiceUnavailable +from sanic.config import Config + +Config.RESPONSE_TIMEOUT = 1 +response_timeout_app = Sanic('test_response_timeout') +response_timeout_default_app = Sanic('test_response_timeout_default') + + +@response_timeout_app.route('/1') +async def handler_1(request): + await asyncio.sleep(2) + return text('OK') + + +@response_timeout_app.exception(ServiceUnavailable) +def handler_exception(request, exception): + return text('Response Timeout from error_handler.', 503) + + +def test_server_error_response_timeout(): + request, response = response_timeout_app.test_client.get('/1') + assert response.status == 503 + assert response.text == 'Response Timeout from error_handler.' + + +@response_timeout_default_app.route('/1') +async def handler_2(request): + await asyncio.sleep(2) + return text('OK') + + +def test_default_server_error_response_timeout(): + request, response = response_timeout_default_app.test_client.get('/1') + assert response.status == 503 + assert response.text == 'Error: Response Timeout' From 1a74accd65cfe6a1c52d3697ab4552285fcd95c9 Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Tue, 12 Sep 2017 13:09:42 +1000 Subject: [PATCH 09/38] finished the keepalive_timeout tests --- tests/test_keep_alive_timeout.py | 164 ++++++++++++++++++++++++++----- 1 file changed, 138 insertions(+), 26 deletions(-) diff --git a/tests/test_keep_alive_timeout.py b/tests/test_keep_alive_timeout.py index 28030144..12e1629d 100644 --- a/tests/test_keep_alive_timeout.py +++ b/tests/test_keep_alive_timeout.py @@ -4,6 +4,7 @@ from time import sleep as sync_sleep import asyncio from sanic.response import text from sanic.config import Config +from sanic import server import aiohttp from aiohttp import TCPConnector from sanic.testing import SanicTestClient, HOST, PORT @@ -12,33 +13,40 @@ from sanic.testing import SanicTestClient, HOST, PORT class ReuseableTCPConnector(TCPConnector): def __init__(self, *args, **kwargs): super(ReuseableTCPConnector, self).__init__(*args, **kwargs) - self.conn = None + self.old_proto = None @asyncio.coroutine def connect(self, req): - if self.conn: - return self.conn - conn = yield from super(ReuseableTCPConnector, self).connect(req) - self.conn = conn - return conn - - def close(self): - return super(ReuseableTCPConnector, self).close() + new_conn = yield from super(ReuseableTCPConnector, self)\ + .connect(req) + if self.old_proto is not None: + if self.old_proto != new_conn.protocol: + raise RuntimeError( + "We got a new connection, wanted the same one!") + self.old_proto = new_conn.protocol + return new_conn class ReuseableSanicTestClient(SanicTestClient): - def __init__(self, app): + def __init__(self, app, loop=None): super(ReuseableSanicTestClient, self).__init__(app) + if loop is None: + loop = asyncio.get_event_loop() + self._loop = loop + self._server = None self._tcp_connector = None self._session = None + # Copied from SanicTestClient, but with some changes to reuse the + # same loop for the same app. def _sanic_endpoint_test( self, method='get', uri='/', gather_request=True, debug=False, server_kwargs={}, *request_args, **request_kwargs): + loop = self._loop results = [None, None] exceptions = [] - + do_kill_server = request_kwargs.pop('end_server', False) if gather_request: def _collect_request(request): if results[0] is None: @@ -47,26 +55,53 @@ class ReuseableSanicTestClient(SanicTestClient): self.app.request_middleware.appendleft(_collect_request) @self.app.listener('after_server_start') - async def _collect_response(sanic, loop): + async def _collect_response(loop): try: + if do_kill_server: + request_kwargs['end_session'] = True response = await self._local_request( method, uri, *request_args, **request_kwargs) results[-1] = response except Exception as e: - log.error( - 'Exception:\n{}'.format(traceback.format_exc())) + import traceback + traceback.print_tb(e.__traceback__) exceptions.append(e) - self.app.stop() + #Don't stop here! self.app.stop() - server = self.app.create_server(host=HOST, debug=debug, port=PORT, **server_kwargs) + if self._server is not None: + _server = self._server + else: + _server_co = self.app.create_server(host=HOST, debug=debug, + port=PORT, **server_kwargs) + + server.trigger_events( + self.app.listeners['before_server_start'], loop) + + try: + loop._stopping = False + http_server = loop.run_until_complete(_server_co) + except Exception as e: + raise e + self._server = _server = http_server + server.trigger_events( + self.app.listeners['after_server_start'], loop) self.app.listeners['after_server_start'].pop() + if do_kill_server: + try: + _server.close() + self._server = None + loop.run_until_complete(_server.wait_closed()) + self.app.stop() + except Exception as e: + exceptions.append(e) if exceptions: raise ValueError( "Exception during request: {}".format(exceptions)) if gather_request: + self.app.request_middleware.pop() try: request, response = results return request, response @@ -81,20 +116,29 @@ class ReuseableSanicTestClient(SanicTestClient): raise ValueError( "Request object expected, got ({})".format(results)) + # Copied from SanicTestClient, but with some changes to reuse the + # same TCPConnection and the sane ClientSession more than once. + # Note, you cannot use the same session if you are in a _different_ + # loop, so the changes above are required too. async def _local_request(self, method, uri, cookies=None, *args, **kwargs): + request_keepalive = kwargs.pop('request_keepalive', + Config.KEEP_ALIVE_TIMEOUT) if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')): url = uri else: url = 'http://{host}:{port}{uri}'.format( host=HOST, port=PORT, uri=uri) + do_kill_session = kwargs.pop('end_session', False) if self._session: session = self._session else: if self._tcp_connector: conn = self._tcp_connector else: - conn = ReuseableTCPConnector(verify_ssl=False) + conn = ReuseableTCPConnector(verify_ssl=False, + keepalive_timeout= + request_keepalive) self._tcp_connector = conn session = aiohttp.ClientSession(cookies=cookies, connector=conn) @@ -115,28 +159,96 @@ class ReuseableSanicTestClient(SanicTestClient): response.json = None response.body = await response.read() - return response + if do_kill_session: + session.close() + self._session = None + return response -Config.KEEP_ALIVE_TIMEOUT = 30 +Config.KEEP_ALIVE_TIMEOUT = 2 Config.KEEP_ALIVE = True -keep_alive_timeout_app = Sanic('test_request_timeout') +keep_alive_timeout_app_reuse = Sanic('test_ka_timeout_reuse') +keep_alive_app_client_timeout = Sanic('test_ka_client_timeout') +keep_alive_app_server_timeout = Sanic('test_ka_server_timeout') -@keep_alive_timeout_app.route('/1') -async def handler(request): +@keep_alive_timeout_app_reuse.route('/1') +async def handler1(request): return text('OK') -def test_keep_alive_timeout(): - client = ReuseableSanicTestClient(keep_alive_timeout_app) +@keep_alive_app_client_timeout.route('/1') +async def handler2(request): + return text('OK') + + +@keep_alive_app_server_timeout.route('/1') +async def handler3(request): + return text('OK') + + +def test_keep_alive_timeout_reuse(): + """If the server keep-alive timeout and client keep-alive timeout are + both longer than the delay, the client _and_ server will successfully + reuse the existing connection.""" + loop = asyncio.get_event_loop() + client = ReuseableSanicTestClient(keep_alive_timeout_app_reuse, loop) headers = { 'Connection': 'keep-alive' } request, response = client.get('/1', headers=headers) assert response.status == 200 - #sync_sleep(2) - request, response = client.get('/1') + assert response.text == 'OK' + sync_sleep(1) + request, response = client.get('/1', end_server=True) assert response.status == 200 + assert response.text == 'OK' +def test_keep_alive_client_timeout(): + """If the server keep-alive timeout is longer than the client + keep-alive timeout, client will try to create a new connection here.""" + loop = asyncio.get_event_loop() + client = ReuseableSanicTestClient(keep_alive_app_client_timeout, + loop) + headers = { + 'Connection': 'keep-alive' + } + request, response = client.get('/1', headers=headers, + request_keepalive=1) + assert response.status == 200 + assert response.text == 'OK' + sync_sleep(3) + exception = None + try: + request, response = client.get('/1', end_server=True) + except ValueError as e: + exception = e + assert exception is not None + assert isinstance(exception, ValueError) + assert "got a new connection" in exception.args[0] + + +def test_keep_alive_server_timeout(): + """If the client keep-alive timeout is longer than the server + keep-alive timeout, the client will get a 'Connection reset' error.""" + loop = asyncio.get_event_loop() + client = ReuseableSanicTestClient(keep_alive_app_server_timeout, + loop) + headers = { + 'Connection': 'keep-alive' + } + request, response = client.get('/1', headers=headers, + request_keepalive=5) + assert response.status == 200 + assert response.text == 'OK' + sync_sleep(3) + exception = None + try: + request, response = client.get('/1', end_server=True) + except ValueError as e: + exception = e + assert exception is not None + assert isinstance(exception, ValueError) + assert "Connection reset" in exception.args[0] + From 173f94216a797041b54a8c4a84ca8e530dbdda4b Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Tue, 12 Sep 2017 13:40:43 +1000 Subject: [PATCH 10/38] Fixed the delays, and expected responses, in the keepalive_timeout tests --- tests/test_keep_alive_timeout.py | 50 ++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 21 deletions(-) diff --git a/tests/test_keep_alive_timeout.py b/tests/test_keep_alive_timeout.py index 12e1629d..09c51d00 100644 --- a/tests/test_keep_alive_timeout.py +++ b/tests/test_keep_alive_timeout.py @@ -1,7 +1,7 @@ from json import JSONDecodeError from sanic import Sanic -from time import sleep as sync_sleep import asyncio +from asyncio import sleep as aio_sleep from sanic.response import text from sanic.config import Config from sanic import server @@ -63,10 +63,8 @@ class ReuseableSanicTestClient(SanicTestClient): method, uri, *request_args, **request_kwargs) results[-1] = response - except Exception as e: - import traceback - traceback.print_tb(e.__traceback__) - exceptions.append(e) + except Exception as e2: + exceptions.append(e2) #Don't stop here! self.app.stop() if self._server is not None: @@ -81,8 +79,8 @@ class ReuseableSanicTestClient(SanicTestClient): try: loop._stopping = False http_server = loop.run_until_complete(_server_co) - except Exception as e: - raise e + except Exception as e1: + raise e1 self._server = _server = http_server server.trigger_events( self.app.listeners['after_server_start'], loop) @@ -94,8 +92,8 @@ class ReuseableSanicTestClient(SanicTestClient): self._server = None loop.run_until_complete(_server.wait_closed()) self.app.stop() - except Exception as e: - exceptions.append(e) + except Exception as e3: + exceptions.append(e3) if exceptions: raise ValueError( "Exception during request: {}".format(exceptions)) @@ -137,11 +135,13 @@ class ReuseableSanicTestClient(SanicTestClient): conn = self._tcp_connector else: conn = ReuseableTCPConnector(verify_ssl=False, + loop=self._loop, keepalive_timeout= request_keepalive) self._tcp_connector = conn session = aiohttp.ClientSession(cookies=cookies, - connector=conn) + connector=conn, + loop=self._loop) self._session = session async with getattr(session, method.lower())( @@ -191,7 +191,8 @@ def test_keep_alive_timeout_reuse(): """If the server keep-alive timeout and client keep-alive timeout are both longer than the delay, the client _and_ server will successfully reuse the existing connection.""" - loop = asyncio.get_event_loop() + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) client = ReuseableSanicTestClient(keep_alive_timeout_app_reuse, loop) headers = { 'Connection': 'keep-alive' @@ -199,7 +200,7 @@ def test_keep_alive_timeout_reuse(): request, response = client.get('/1', headers=headers) assert response.status == 200 assert response.text == 'OK' - sync_sleep(1) + loop.run_until_complete(aio_sleep(1)) request, response = client.get('/1', end_server=True) assert response.status == 200 assert response.text == 'OK' @@ -208,7 +209,8 @@ def test_keep_alive_timeout_reuse(): def test_keep_alive_client_timeout(): """If the server keep-alive timeout is longer than the client keep-alive timeout, client will try to create a new connection here.""" - loop = asyncio.get_event_loop() + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) client = ReuseableSanicTestClient(keep_alive_app_client_timeout, loop) headers = { @@ -218,10 +220,11 @@ def test_keep_alive_client_timeout(): request_keepalive=1) assert response.status == 200 assert response.text == 'OK' - sync_sleep(3) + loop.run_until_complete(aio_sleep(2)) exception = None try: - request, response = client.get('/1', end_server=True) + request, response = client.get('/1', end_server=True, + request_keepalive=1) except ValueError as e: exception = e assert exception is not None @@ -231,24 +234,29 @@ def test_keep_alive_client_timeout(): def test_keep_alive_server_timeout(): """If the client keep-alive timeout is longer than the server - keep-alive timeout, the client will get a 'Connection reset' error.""" - loop = asyncio.get_event_loop() + keep-alive timeout, the client will either a 'Connection reset' error + _or_ a new connection. Depending on how the event-loop handles the + broken server connection.""" + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) client = ReuseableSanicTestClient(keep_alive_app_server_timeout, loop) headers = { 'Connection': 'keep-alive' } request, response = client.get('/1', headers=headers, - request_keepalive=5) + request_keepalive=60) assert response.status == 200 assert response.text == 'OK' - sync_sleep(3) + loop.run_until_complete(aio_sleep(3)) exception = None try: - request, response = client.get('/1', end_server=True) + request, response = client.get('/1', request_keepalive=60, + end_server=True) except ValueError as e: exception = e assert exception is not None assert isinstance(exception, ValueError) - assert "Connection reset" in exception.args[0] + assert "Connection reset" in exception.args[0] or \ + "got a new connection" in exception.args[0] From a46e004f07600f9041da1c05d640bd870a643a4c Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Mon, 11 Sep 2017 22:12:49 -0700 Subject: [PATCH 11/38] apply new loggers --- sanic/__main__.py | 6 +++--- sanic/app.py | 14 +++++++------- sanic/handlers.py | 8 ++++---- sanic/log.py | 11 +++-------- sanic/request.py | 4 ++-- sanic/server.py | 34 +++++++++++++++++----------------- sanic/testing.py | 6 +++--- 7 files changed, 39 insertions(+), 44 deletions(-) diff --git a/sanic/__main__.py b/sanic/__main__.py index cc580566..1473aa5f 100644 --- a/sanic/__main__.py +++ b/sanic/__main__.py @@ -1,7 +1,7 @@ from argparse import ArgumentParser from importlib import import_module -from sanic.log import log +from sanic.log import logger from sanic.app import Sanic if __name__ == "__main__": @@ -36,9 +36,9 @@ if __name__ == "__main__": app.run(host=args.host, port=args.port, workers=args.workers, debug=args.debug, ssl=ssl) except ImportError as e: - log.error("No module named {} found.\n" + logger.error("No module named {} found.\n" " Example File: project/sanic_server.py -> app\n" " Example Module: project.sanic_server.app" .format(e.name)) except ValueError as e: - log.error("{}".format(e)) + logger.error("{}".format(e)) diff --git a/sanic/app.py b/sanic/app.py index 595b50c1..4c0fa15e 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -14,7 +14,7 @@ from sanic.config import Config from sanic.constants import HTTP_METHODS from sanic.exceptions import ServerError, URLBuildError, SanicException from sanic.handlers import ErrorHandler -from sanic.log import log +from sanic.log import logger, error_logger from sanic.response import HTTPResponse, StreamingHTTPResponse from sanic.router import Router from sanic.server import serve, serve_multiple, HttpProtocol, Signal @@ -542,7 +542,7 @@ class Sanic: response = await self._run_response_middleware(request, response) except: - log.exception( + error_logger.exception( 'Exception occured in one of response middleware handlers' ) @@ -609,12 +609,12 @@ class Sanic: else: serve_multiple(server_settings, workers) except: - log.exception( + error_logger.exception( 'Experienced exception while trying to serve') raise finally: self.is_running = False - log.info("Server Stopped") + logger.info("Server Stopped") def stop(self): """This kills the Sanic""" @@ -757,9 +757,9 @@ class Sanic: server_settings[settings_name] = listeners if debug: - log.setLevel(logging.DEBUG) + logger.setLevel(logging.DEBUG) if self.config.LOGO is not None: - log.debug(self.config.LOGO) + logger.debug(self.config.LOGO) if run_async: server_settings['run_async'] = True @@ -769,6 +769,6 @@ class Sanic: proto = "http" if ssl is not None: proto = "https" - log.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port)) + logger.info('Goin\' Fast @ {}://{}:{}'.format(proto, host, port)) return server_settings diff --git a/sanic/handlers.py b/sanic/handlers.py index 6a87fd5d..2e335b09 100644 --- a/sanic/handlers.py +++ b/sanic/handlers.py @@ -12,7 +12,7 @@ from sanic.exceptions import ( TRACEBACK_WRAPPER_HTML, TRACEBACK_WRAPPER_INNER_HTML, TRACEBACK_BORDER) -from sanic.log import log +from sanic.log import logger from sanic.response import text, html @@ -90,7 +90,7 @@ class ErrorHandler: 'Exception raised in exception handler "{}" ' 'for uri: "{}"\n{}').format( handler.__name__, url, format_exc()) - log.error(response_message) + logger.error(response_message) return text(response_message, 500) else: return text('An error occurred while handling an error', 500) @@ -101,7 +101,7 @@ class ErrorHandler: Override this method in an ErrorHandler subclass to prevent logging exceptions. """ - getattr(log, level)(message) + getattr(logger, level)(message) def default(self, request, exception): self.log(format_exc()) @@ -117,7 +117,7 @@ class ErrorHandler: response_message = ( 'Exception occurred while handling uri: "{}"\n{}'.format( request.url, format_exc())) - log.error(response_message) + logger.error(response_message) return html(html_output, status=500) else: return html(INTERNAL_SERVER_ERROR_HTML, status=500) diff --git a/sanic/log.py b/sanic/log.py index 3d254d6c..5abce0e2 100644 --- a/sanic/log.py +++ b/sanic/log.py @@ -8,7 +8,8 @@ LOGGING_CONFIG_DEFAULTS = dict( loggers={ "root": { "level": "INFO", - "handlers": ["console"]}, + "handlers": ["console"] + }, "sanic.error": { "level": "INFO", "handlers": ["error_console"], @@ -45,12 +46,6 @@ LOGGING_CONFIG_DEFAULTS = dict( ) -class AccessLogger: - - def __init__(self, logger, access_log_format=None): - pass - - -log = logging.getLogger('sanic') +logger = logging.getLogger('root') error_logger = logging.getLogger('sanic.error') access_logger = logging.getLogger('sanic.access') diff --git a/sanic/request.py b/sanic/request.py index 27ff011e..b54d463f 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -17,7 +17,7 @@ except ImportError: json_loads = json.loads from sanic.exceptions import InvalidUsage -from sanic.log import log +from sanic.log import logger DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream" @@ -114,7 +114,7 @@ class Request(dict): self.parsed_form, self.parsed_files = ( parse_multipart_form(self.body, boundary)) except Exception: - log.exception("Failed when parsing form") + logger.exception("Failed when parsing form") return self.parsed_form diff --git a/sanic/server.py b/sanic/server.py index 3e52d634..7764b120 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -24,7 +24,7 @@ try: except ImportError: async_loop = asyncio -from sanic.log import log, netlog +from sanic.log import logger, access_logger, error_logger from sanic.response import HTTPResponse from sanic.request import Request from sanic.exceptions import ( @@ -247,7 +247,7 @@ class HttpProtocol(asyncio.Protocol): self.request.version, keep_alive, self.request_timeout)) if self.access_log: - netlog.info('', extra={ + access_logger.info('', extra={ 'status': response.status, 'byte': len(response.body), 'host': '{0}:{1}'.format(self.request.ip[0], @@ -256,13 +256,13 @@ class HttpProtocol(asyncio.Protocol): self.request.url) }) except AttributeError: - log.error( + logger.error( ('Invalid response object for url {}, ' 'Expected Type: HTTPResponse, Actual Type: {}').format( self.url, type(response))) self.write_error(ServerError('Invalid response type')) except RuntimeError: - log.error( + logger.error( 'Connection lost before response written @ {}'.format( self.request.ip)) except Exception as e: @@ -289,7 +289,7 @@ class HttpProtocol(asyncio.Protocol): await response.stream( self.request.version, keep_alive, self.request_timeout) if self.access_log: - netlog.info('', extra={ + access_logger.info('', extra={ 'status': response.status, 'byte': -1, 'host': '{0}:{1}'.format(self.request.ip[0], @@ -298,13 +298,13 @@ class HttpProtocol(asyncio.Protocol): self.request.url) }) except AttributeError: - log.error( + logger.error( ('Invalid response object for url {}, ' 'Expected Type: HTTPResponse, Actual Type: {}').format( self.url, type(response))) self.write_error(ServerError('Invalid response type')) except RuntimeError: - log.error( + logger.error( 'Connection lost before response written @ {}'.format( self.request.ip)) except Exception as e: @@ -325,7 +325,7 @@ class HttpProtocol(asyncio.Protocol): version = self.request.version if self.request else '1.1' self.transport.write(response.output(version)) except RuntimeError: - log.error( + logger.error( 'Connection lost before error written @ {}'.format( self.request.ip if self.request else 'Unknown')) except Exception as e: @@ -350,21 +350,21 @@ class HttpProtocol(asyncio.Protocol): extra['request'] = 'nil' if self.parser and not (self.keep_alive and extra['status'] == 408): - netlog.info('', extra=extra) + access_logger.info('', extra=extra) self.transport.close() def bail_out(self, message, from_error=False): if from_error or self.transport.is_closing(): - log.error( + logger.error( ("Transport closed @ {} and exception " "experienced during error handling").format( self.transport.get_extra_info('peername'))) - log.debug( + logger.debug( 'Exception:\n{}'.format(traceback.format_exc())) else: exception = ServerError(message) self.write_error(exception) - log.error(message) + logger.error(message) def cleanup(self): self.parser = None @@ -508,7 +508,7 @@ def serve(host, port, request_handler, error_handler, before_start=None, try: http_server = loop.run_until_complete(server_coroutine) except: - log.exception("Unable to start server") + logger.exception("Unable to start server") return trigger_events(after_start, loop) @@ -519,14 +519,14 @@ def serve(host, port, request_handler, error_handler, before_start=None, try: loop.add_signal_handler(_signal, loop.stop) except NotImplementedError: - log.warn('Sanic tried to use loop.add_signal_handler but it is' + logger.warn('Sanic tried to use loop.add_signal_handler but it is' ' not implemented on this platform.') pid = os.getpid() try: - log.info('Starting worker [{}]'.format(pid)) + logger.info('Starting worker [{}]'.format(pid)) loop.run_forever() finally: - log.info("Stopping worker [{}]".format(pid)) + logger.info("Stopping worker [{}]".format(pid)) # Run the on_stop function if provided trigger_events(before_stop, loop) @@ -588,7 +588,7 @@ def serve_multiple(server_settings, workers): server_settings['port'] = None def sig_handler(signal, frame): - log.info("Received signal {}. Shutting down.".format( + logger.info("Received signal {}. Shutting down.".format( Signals(signal).name)) for process in processes: os.kill(process.pid, SIGINT) diff --git a/sanic/testing.py b/sanic/testing.py index de26d025..5d233d7b 100644 --- a/sanic/testing.py +++ b/sanic/testing.py @@ -1,7 +1,7 @@ import traceback from json import JSONDecodeError -from sanic.log import log +from sanic.log import logger HOST = '127.0.0.1' PORT = 42101 @@ -19,7 +19,7 @@ class SanicTestClient: url = 'http://{host}:{port}{uri}'.format( host=HOST, port=PORT, uri=uri) - log.info(url) + logger.info(url) conn = aiohttp.TCPConnector(verify_ssl=False) async with aiohttp.ClientSession( cookies=cookies, connector=conn) as session: @@ -61,7 +61,7 @@ class SanicTestClient: **request_kwargs) results[-1] = response except Exception as e: - log.error( + logger.error( 'Exception:\n{}'.format(traceback.format_exc())) exceptions.append(e) self.app.stop() From 8eb59ad4dc27261351ea9d1eb7f645ee4f0a9a40 Mon Sep 17 00:00:00 2001 From: Ashley Sommer Date: Wed, 13 Sep 2017 10:18:36 +1000 Subject: [PATCH 12/38] Fixed error where the RequestTimeout test wasn't actually testing the correct behaviour Fixed error where KeepAliveTimeout wasn't being triggered in the test suite, when using uvloop Fixed test cases when using other asyncio loops such as uvloop Fixed Flake8 linting errors --- sanic/config.py | 2 +- sanic/server.py | 1 - tests/test_keep_alive_timeout.py | 13 +++- tests/test_request_timeout.py | 102 +++++++++++++++++++++++++------ 4 files changed, 95 insertions(+), 23 deletions(-) diff --git a/sanic/config.py b/sanic/config.py index 560fa2ec..de91280f 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -129,7 +129,7 @@ class Config(dict): self.KEEP_ALIVE = keep_alive # Apache httpd server default keepalive timeout = 5 seconds # Nginx server default keepalive timeout = 75 seconds - # Nginx performance tuning guidelines uses keepalive timeout = 15 seconds + # Nginx performance tuning guidelines uses keepalive = 15 seconds # IE client hard keepalive limit = 60 seconds # Firefox client hard keepalive limit = 115 seconds diff --git a/sanic/server.py b/sanic/server.py index bcef8a91..eb9864cd 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -193,7 +193,6 @@ class HttpProtocol(asyncio.Protocol): log.info('KeepAlive Timeout. Closing connection.') self.transport.close() - # -------------------------------------------- # # Parsing # -------------------------------------------- # diff --git a/tests/test_keep_alive_timeout.py b/tests/test_keep_alive_timeout.py index 09c51d00..15f6d705 100644 --- a/tests/test_keep_alive_timeout.py +++ b/tests/test_keep_alive_timeout.py @@ -20,10 +20,11 @@ class ReuseableTCPConnector(TCPConnector): new_conn = yield from super(ReuseableTCPConnector, self)\ .connect(req) if self.old_proto is not None: - if self.old_proto != new_conn.protocol: + if self.old_proto != new_conn._protocol: raise RuntimeError( "We got a new connection, wanted the same one!") - self.old_proto = new_conn.protocol + print(new_conn.__dict__) + self.old_proto = new_conn._protocol return new_conn @@ -64,6 +65,8 @@ class ReuseableSanicTestClient(SanicTestClient): **request_kwargs) results[-1] = response except Exception as e2: + import traceback + traceback.print_tb(e2.__traceback__) exceptions.append(e2) #Don't stop here! self.app.stop() @@ -80,6 +83,8 @@ class ReuseableSanicTestClient(SanicTestClient): loop._stopping = False http_server = loop.run_until_complete(_server_co) except Exception as e1: + import traceback + traceback.print_tb(e1.__traceback__) raise e1 self._server = _server = http_server server.trigger_events( @@ -93,7 +98,9 @@ class ReuseableSanicTestClient(SanicTestClient): loop.run_until_complete(_server.wait_closed()) self.app.stop() except Exception as e3: - exceptions.append(e3) + import traceback + traceback.print_tb(e3.__traceback__) + exceptions.append(e3) if exceptions: raise ValueError( "Exception during request: {}".format(exceptions)) diff --git a/tests/test_request_timeout.py b/tests/test_request_timeout.py index e6c1f657..a1d8a885 100644 --- a/tests/test_request_timeout.py +++ b/tests/test_request_timeout.py @@ -1,8 +1,8 @@ from json import JSONDecodeError + from sanic import Sanic import asyncio from sanic.response import text -from sanic.exceptions import RequestTimeout from sanic.config import Config import aiohttp from aiohttp import TCPConnector @@ -10,21 +10,68 @@ from sanic.testing import SanicTestClient, HOST, PORT class DelayableTCPConnector(TCPConnector): - class DelayableHttpRequest(object): + + class RequestContextManager(object): def __new__(cls, req, delay): - cls = super(DelayableTCPConnector.DelayableHttpRequest, cls).\ + cls = super(DelayableTCPConnector.RequestContextManager, cls).\ __new__(cls) cls.req = req + cls.send_task = None + cls.resp = None + cls.orig_send = getattr(req, 'send') + cls.orig_start = None cls.delay = delay + cls._acting_as = req return cls def __getattr__(self, item): - return getattr(self.req, item) + acting_as = self._acting_as + return getattr(acting_as, item) + + @asyncio.coroutine + def start(self, connection, read_until_eof=False): + if self.send_task is None: + raise RuntimeError("do a send() before you do a start()") + resp = yield from self.send_task + self.send_task = None + self.resp = resp + self._acting_as = self.resp + self.orig_start = getattr(resp, 'start') + + try: + ret = yield from self.orig_start(connection, + read_until_eof) + except Exception as e: + raise e + return ret + + def close(self): + if self.resp is not None: + self.resp.close() + if self.send_task is not None: + self.send_task.cancel() + + @asyncio.coroutine + def delayed_send(self, *args, **kwargs): + req = self.req + if self.delay and self.delay > 0: + #sync_sleep(self.delay) + _ = yield from asyncio.sleep(self.delay) + t = req.loop.time() + print("sending at {}".format(t), flush=True) + conn = next(iter(args)) # first arg is connection + try: + delayed_resp = self.orig_send(*args, **kwargs) + except Exception as e: + return aiohttp.ClientResponse(req.method, req.url) + return delayed_resp def send(self, *args, **kwargs): - if self.delay and self.delay > 0: - _ = yield from asyncio.sleep(self.delay) - self.req.send(*args, **kwargs) + gen = self.delayed_send(*args, **kwargs) + task = self.req.loop.create_task(gen) + self.send_task = task + self._acting_as = task + return self def __init__(self, *args, **kwargs): _post_connect_delay = kwargs.pop('post_connect_delay', 0) @@ -35,31 +82,37 @@ class DelayableTCPConnector(TCPConnector): @asyncio.coroutine def connect(self, req): - req = DelayableTCPConnector.\ - DelayableHttpRequest(req, self._pre_request_delay) + d_req = DelayableTCPConnector.\ + RequestContextManager(req, self._pre_request_delay) conn = yield from super(DelayableTCPConnector, self).connect(req) if self._post_connect_delay and self._post_connect_delay > 0: - _ = yield from asyncio.sleep(self._post_connect_delay) + _ = yield from asyncio.sleep(self._post_connect_delay, + loop=self._loop) + req.send = d_req.send + t = req.loop.time() + print("Connected at {}".format(t), flush=True) return conn class DelayableSanicTestClient(SanicTestClient): - def __init__(self, app, request_delay=1): + def __init__(self, app, loop, request_delay=1): super(DelayableSanicTestClient, self).__init__(app) self._request_delay = request_delay + self._loop = None async def _local_request(self, method, uri, cookies=None, *args, **kwargs): + if self._loop is None: + self._loop = asyncio.get_event_loop() if uri.startswith(('http:', 'https:', 'ftp:', 'ftps://' '//')): url = uri else: url = 'http://{host}:{port}{uri}'.format( host=HOST, port=PORT, uri=uri) - conn = DelayableTCPConnector(pre_request_delay=self._request_delay, - verify_ssl=False) - async with aiohttp.ClientSession( - cookies=cookies, connector=conn) as session: + verify_ssl=False, loop=self._loop) + async with aiohttp.ClientSession(cookies=cookies, connector=conn, + loop=self._loop) as session: # Insert a delay after creating the connection # But before sending the request. @@ -81,17 +134,30 @@ class DelayableSanicTestClient(SanicTestClient): return response -Config.REQUEST_TIMEOUT = 1 +Config.REQUEST_TIMEOUT = 2 request_timeout_default_app = Sanic('test_request_timeout_default') +request_no_timeout_app = Sanic('test_request_no_timeout') @request_timeout_default_app.route('/1') -async def handler(request): +async def handler1(request): + return text('OK') + + +@request_no_timeout_app.route('/1') +async def handler2(request): return text('OK') def test_default_server_error_request_timeout(): - client = DelayableSanicTestClient(request_timeout_default_app, 2) + client = DelayableSanicTestClient(request_timeout_default_app, None, 3) request, response = client.get('/1') assert response.status == 408 assert response.text == 'Error: Request Timeout' + + +def test_default_server_error_request_dont_timeout(): + client = DelayableSanicTestClient(request_no_timeout_app, None, 1) + request, response = client.get('/1') + assert response.status == 200 + assert response.text == 'OK' From 24bdb1ce98d8c918cc1503892cd148a4e5b7c3d7 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Tue, 12 Sep 2017 23:42:42 -0700 Subject: [PATCH 13/38] add unit tests/refactoring --- sanic/app.py | 15 +++++++++++---- sanic/config.py | 8 +------- sanic/log.py | 15 +++++++++++++-- sanic/request.py | 4 ++-- tests/test_logging.py | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 61 insertions(+), 15 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 4c0fa15e..1d4497d0 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -14,7 +14,7 @@ from sanic.config import Config from sanic.constants import HTTP_METHODS from sanic.exceptions import ServerError, URLBuildError, SanicException from sanic.handlers import ErrorHandler -from sanic.log import logger, error_logger +from sanic.log import logger, error_logger, LOGGING_CONFIG_DEFAULTS from sanic.response import HTTPResponse, StreamingHTTPResponse from sanic.router import Router from sanic.server import serve, serve_multiple, HttpProtocol, Signal @@ -28,13 +28,16 @@ class Sanic: def __init__(self, name=None, router=None, error_handler=None, load_env=True, request_class=None, - strict_slashes=False): + strict_slashes=False, log_config=None): # Get name from previous stack frame if name is None: frame_records = stack()[1] name = getmodulename(frame_records[1]) + # logging + logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS) + self.name = name self.router = router or Router() self.request_class = request_class @@ -567,7 +570,7 @@ class Sanic: def run(self, host=None, port=None, debug=False, ssl=None, sock=None, workers=1, protocol=None, backlog=100, stop_event=None, register_sys_signals=True, - access_log=True): + access_log=True, log_config=None): """Run the HTTP Server and listen until keyboard interrupt or term signal. On termination, drain connections before closing. @@ -585,6 +588,8 @@ class Sanic: :param protocol: Subclass of asyncio protocol class :return: Nothing """ + logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS) + if sock is None: host, port = host or "127.0.0.1", port or 8000 @@ -627,12 +632,14 @@ class Sanic: async def create_server(self, host=None, port=None, debug=False, ssl=None, sock=None, protocol=None, backlog=100, stop_event=None, - access_log=True): + access_log=True, log_config=None): """Asynchronous version of `run`. NOTE: This does not support multiprocessing and is not the preferred way to run a Sanic application. """ + logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS) + if sock is None: host, port = host or "127.0.0.1", port or 8000 diff --git a/sanic/config.py b/sanic/config.py index 1f0bbd3e..b5430445 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -4,14 +4,8 @@ import syslog import platform import types -SANIC_PREFIX = 'SANIC_' -_address_dict = { - 'Windows': ('localhost', 514), - 'Darwin': '/var/run/syslog', - 'Linux': '/dev/log', - 'FreeBSD': '/var/run/log' -} +SANIC_PREFIX = 'SANIC_' class Config(dict): diff --git a/sanic/log.py b/sanic/log.py index 5abce0e2..72636cd0 100644 --- a/sanic/log.py +++ b/sanic/log.py @@ -19,7 +19,7 @@ LOGGING_CONFIG_DEFAULTS = dict( "sanic.access": { "level": "INFO", - "handlers": ["console"], + "handlers": ["access_console"], "propagate": True, "qualname": "sanic.access" } @@ -35,13 +35,24 @@ LOGGING_CONFIG_DEFAULTS = dict( "formatter": "generic", "stream": "sys.stderr" }, + "access_console": { + "class": "logging.StreamHandler", + "formatter": "access", + "stream": "sys.stdout" + }, }, formatters={ "generic": { "format": "%(asctime)s [%(process)d] [%(levelname)s] %(message)s", "datefmt": "[%Y-%m-%d %H:%M:%S %z]", "class": "logging.Formatter" - } + }, + "access": { + "format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: " + + "%(request)s %(message)s %(status)d %(byte)d", + "datefmt": "[%Y-%m-%d %H:%M:%S %z]", + "class": "logging.Formatter" + }, } ) diff --git a/sanic/request.py b/sanic/request.py index b54d463f..c842cb4b 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -17,7 +17,7 @@ except ImportError: json_loads = json.loads from sanic.exceptions import InvalidUsage -from sanic.log import logger +from sanic.log import error_logger DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream" @@ -114,7 +114,7 @@ class Request(dict): self.parsed_form, self.parsed_files = ( parse_multipart_form(self.body, boundary)) except Exception: - logger.exception("Failed when parsing form") + error_logger.exception("Failed when parsing form") return self.parsed_form diff --git a/tests/test_logging.py b/tests/test_logging.py index 3d75dbe0..112c94a0 100644 --- a/tests/test_logging.py +++ b/tests/test_logging.py @@ -2,6 +2,7 @@ import uuid from importlib import reload from sanic.response import text +from sanic.log import LOGGING_CONFIG_DEFAULTS from sanic import Sanic from io import StringIO import logging @@ -37,3 +38,36 @@ def test_log(): request, response = app.test_client.get('/') log_text = log_stream.getvalue() assert rand_string in log_text + + +def test_logging_defaults(): + reset_logging() + app = Sanic("test_logging") + + for fmt in [h.formatter for h in logging.getLogger('root').handlers]: + assert fmt._fmt == LOGGING_CONFIG_DEFAULTS['formatters']['generic']['format'] + + for fmt in [h.formatter for h in logging.getLogger('sanic.error').handlers]: + assert fmt._fmt == LOGGING_CONFIG_DEFAULTS['formatters']['generic']['format'] + + for fmt in [h.formatter for h in logging.getLogger('sanic.access').handlers]: + assert fmt._fmt == LOGGING_CONFIG_DEFAULTS['formatters']['access']['format'] + + +def test_logging_pass_customer_logconfig(): + reset_logging() + + modified_config = LOGGING_CONFIG_DEFAULTS + modified_config['formatters']['generic']['format'] = '%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s' + modified_config['formatters']['access']['format'] = '%(asctime)s - (%(name)s)[%(levelname)s]: %(message)s' + + app = Sanic("test_logging", log_config=modified_config) + + for fmt in [h.formatter for h in logging.getLogger('root').handlers]: + assert fmt._fmt == modified_config['formatters']['generic']['format'] + + for fmt in [h.formatter for h in logging.getLogger('sanic.error').handlers]: + assert fmt._fmt == modified_config['formatters']['generic']['format'] + + for fmt in [h.formatter for h in logging.getLogger('sanic.access').handlers]: + assert fmt._fmt == modified_config['formatters']['access']['format'] From 2e5d1ddff985da5ec116d50560eca323188f647a Mon Sep 17 00:00:00 2001 From: aiosin Date: Wed, 13 Sep 2017 14:08:29 +0200 Subject: [PATCH 14/38] add status codes and teapot example --- examples/teapot.py | 13 +++++++++++++ sanic/response.py | 2 ++ 2 files changed, 15 insertions(+) create mode 100644 examples/teapot.py diff --git a/examples/teapot.py b/examples/teapot.py new file mode 100644 index 00000000..897f7836 --- /dev/null +++ b/examples/teapot.py @@ -0,0 +1,13 @@ +from sanic import Sanic +from sanic import response as res + +app = Sanic(__name__) + + +@app.route("/") +async def test(req): + return res.text("I\'m a teapot", status=418) + + +if __name__ == '__main__': + app.run(host="0.0.0.0", port=8000) diff --git a/sanic/response.py b/sanic/response.py index 902b21c6..3b0ef449 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -56,6 +56,7 @@ ALL_STATUS_CODES = { 415: b'Unsupported Media Type', 416: b'Requested Range Not Satisfiable', 417: b'Expectation Failed', + 418: b'I\'m a teapot', 422: b'Unprocessable Entity', 423: b'Locked', 424: b'Failed Dependency', @@ -63,6 +64,7 @@ ALL_STATUS_CODES = { 428: b'Precondition Required', 429: b'Too Many Requests', 431: b'Request Header Fields Too Large', + 451: b'Unavailable For Legal Reasons', 500: b'Internal Server Error', 501: b'Not Implemented', 502: b'Bad Gateway', From 9c4b0f7b15f240f81b249848e649278226eb0a62 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Wed, 13 Sep 2017 07:40:42 -0700 Subject: [PATCH 15/38] fix flake8 --- sanic/__main__.py | 6 +-- sanic/config.py | 3 -- sanic/log.py | 98 +++++++++++++++++++++++------------------------ sanic/server.py | 13 ++++--- 4 files changed, 59 insertions(+), 61 deletions(-) diff --git a/sanic/__main__.py b/sanic/__main__.py index 1473aa5f..594256f8 100644 --- a/sanic/__main__.py +++ b/sanic/__main__.py @@ -37,8 +37,8 @@ if __name__ == "__main__": workers=args.workers, debug=args.debug, ssl=ssl) except ImportError as e: logger.error("No module named {} found.\n" - " Example File: project/sanic_server.py -> app\n" - " Example Module: project.sanic_server.app" - .format(e.name)) + " Example File: project/sanic_server.py -> app\n" + " Example Module: project.sanic_server.app" + .format(e.name)) except ValueError as e: logger.error("{}".format(e)) diff --git a/sanic/config.py b/sanic/config.py index b5430445..a79c1c12 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -1,7 +1,4 @@ import os -import sys -import syslog -import platform import types diff --git a/sanic/log.py b/sanic/log.py index 72636cd0..f9f96005 100644 --- a/sanic/log.py +++ b/sanic/log.py @@ -2,58 +2,58 @@ import logging LOGGING_CONFIG_DEFAULTS = dict( - version=1, - disable_existing_loggers=False, + version=1, + disable_existing_loggers=False, - loggers={ - "root": { - "level": "INFO", - "handlers": ["console"] - }, - "sanic.error": { - "level": "INFO", - "handlers": ["error_console"], - "propagate": True, - "qualname": "sanic.error" - }, + loggers={ + "root": { + "level": "INFO", + "handlers": ["console"] + }, + "sanic.error": { + "level": "INFO", + "handlers": ["error_console"], + "propagate": True, + "qualname": "sanic.error" + }, - "sanic.access": { - "level": "INFO", - "handlers": ["access_console"], - "propagate": True, - "qualname": "sanic.access" - } - }, - handlers={ - "console": { - "class": "logging.StreamHandler", - "formatter": "generic", - "stream": "sys.stdout" - }, - "error_console": { - "class": "logging.StreamHandler", - "formatter": "generic", - "stream": "sys.stderr" - }, - "access_console": { - "class": "logging.StreamHandler", - "formatter": "access", - "stream": "sys.stdout" - }, - }, - formatters={ - "generic": { - "format": "%(asctime)s [%(process)d] [%(levelname)s] %(message)s", - "datefmt": "[%Y-%m-%d %H:%M:%S %z]", - "class": "logging.Formatter" - }, - "access": { - "format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: " + - "%(request)s %(message)s %(status)d %(byte)d", - "datefmt": "[%Y-%m-%d %H:%M:%S %z]", - "class": "logging.Formatter" - }, + "sanic.access": { + "level": "INFO", + "handlers": ["access_console"], + "propagate": True, + "qualname": "sanic.access" } + }, + handlers={ + "console": { + "class": "logging.StreamHandler", + "formatter": "generic", + "stream": "sys.stdout" + }, + "error_console": { + "class": "logging.StreamHandler", + "formatter": "generic", + "stream": "sys.stderr" + }, + "access_console": { + "class": "logging.StreamHandler", + "formatter": "access", + "stream": "sys.stdout" + }, + }, + formatters={ + "generic": { + "format": "%(asctime)s [%(process)d] [%(levelname)s] %(message)s", + "datefmt": "[%Y-%m-%d %H:%M:%S %z]", + "class": "logging.Formatter" + }, + "access": { + "format": "%(asctime)s - (%(name)s)[%(levelname)s][%(host)s]: " + + "%(request)s %(message)s %(status)d %(byte)d", + "datefmt": "[%Y-%m-%d %H:%M:%S %z]", + "class": "logging.Formatter" + }, + } ) diff --git a/sanic/server.py b/sanic/server.py index 7764b120..75c4ee55 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -24,7 +24,7 @@ try: except ImportError: async_loop = asyncio -from sanic.log import logger, access_logger, error_logger +from sanic.log import logger, access_logger from sanic.response import HTTPResponse from sanic.request import Request from sanic.exceptions import ( @@ -424,9 +424,9 @@ def serve(host, port, request_handler, error_handler, before_start=None, request_timeout=60, ssl=None, sock=None, request_max_size=None, reuse_port=False, loop=None, protocol=HttpProtocol, backlog=100, register_sys_signals=True, run_async=False, connections=None, - signal=Signal(), request_class=None, access_log=True, keep_alive=True, - is_request_stream=False, router=None, websocket_max_size=None, - websocket_max_queue=None, state=None, + signal=Signal(), request_class=None, access_log=True, + keep_alive=True, is_request_stream=False, router=None, + websocket_max_size=None, websocket_max_queue=None, state=None, graceful_shutdown_timeout=15.0): """Start asynchronous HTTP Server on an individual process. @@ -519,8 +519,9 @@ def serve(host, port, request_handler, error_handler, before_start=None, try: loop.add_signal_handler(_signal, loop.stop) except NotImplementedError: - logger.warn('Sanic tried to use loop.add_signal_handler but it is' - ' not implemented on this platform.') + logger.warn( + 'Sanic tried to use loop.add_signal_handler but it is' + ' not implemented on this platform.') pid = os.getpid() try: logger.info('Starting worker [{}]'.format(pid)) From 5ee7b6caeba6b39ac0400c59ed9a16c0de316427 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Wed, 13 Sep 2017 10:35:34 -0700 Subject: [PATCH 16/38] fixing small issue --- sanic/app.py | 6 ++---- sanic/log.py | 7 ++++--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 1d4497d0..f06e9cd4 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -570,7 +570,7 @@ class Sanic: def run(self, host=None, port=None, debug=False, ssl=None, sock=None, workers=1, protocol=None, backlog=100, stop_event=None, register_sys_signals=True, - access_log=True, log_config=None): + access_log=True): """Run the HTTP Server and listen until keyboard interrupt or term signal. On termination, drain connections before closing. @@ -588,7 +588,6 @@ class Sanic: :param protocol: Subclass of asyncio protocol class :return: Nothing """ - logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS) if sock is None: host, port = host or "127.0.0.1", port or 8000 @@ -632,13 +631,12 @@ class Sanic: async def create_server(self, host=None, port=None, debug=False, ssl=None, sock=None, protocol=None, backlog=100, stop_event=None, - access_log=True, log_config=None): + access_log=True): """Asynchronous version of `run`. NOTE: This does not support multiprocessing and is not the preferred way to run a Sanic application. """ - logging.config.dictConfig(log_config or LOGGING_CONFIG_DEFAULTS) if sock is None: host, port = host or "127.0.0.1", port or 8000 diff --git a/sanic/log.py b/sanic/log.py index f9f96005..9c6d868d 100644 --- a/sanic/log.py +++ b/sanic/log.py @@ -1,4 +1,5 @@ import logging +import sys LOGGING_CONFIG_DEFAULTS = dict( @@ -28,17 +29,17 @@ LOGGING_CONFIG_DEFAULTS = dict( "console": { "class": "logging.StreamHandler", "formatter": "generic", - "stream": "sys.stdout" + "stream": sys.stdout }, "error_console": { "class": "logging.StreamHandler", "formatter": "generic", - "stream": "sys.stderr" + "stream": sys.stderr }, "access_console": { "class": "logging.StreamHandler", "formatter": "access", - "stream": "sys.stdout" + "stream": sys.stdout }, }, formatters={ From ddc039ed2e0df68908f2a9c673c6440f2704fa8d Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Wed, 13 Sep 2017 18:14:46 -0700 Subject: [PATCH 17/38] update doc --- docs/sanic/logging.md | 65 ++++++++----------------------------------- 1 file changed, 12 insertions(+), 53 deletions(-) diff --git a/docs/sanic/logging.md b/docs/sanic/logging.md index eb807388..092df5c7 100644 --- a/docs/sanic/logging.md +++ b/docs/sanic/logging.md @@ -9,12 +9,6 @@ A simple example using default settings would be like this: ```python from sanic import Sanic -from sanic.config import LOGGING - -# The default logging handlers are ['accessStream', 'errorStream'] -# but we change it to use other handlers here for demo purpose -LOGGING['loggers']['network']['handlers'] = [ - 'accessSysLog', 'errorSysLog'] app = Sanic('test') @@ -23,14 +17,14 @@ async def test(request): return response.text('Hello World!') if __name__ == "__main__": - app.run(log_config=LOGGING) + app.run(debug=True, access_log=True) ``` -And to close logging, simply assign log_config=None: +And to close logging, simply assign access_log=False: ```python if __name__ == "__main__": - app.run(log_config=None) + app.run(access_log=False) ``` This would skip calling logging functions when handling requests. @@ -38,59 +32,24 @@ And you could even do further in production to gain extra speed: ```python if __name__ == "__main__": - # disable internal messages - app.run(debug=False, log_config=None) + # disable debug messages + app.run(debug=False, access_log=False) ``` ### Configuration -By default, log_config parameter is set to use sanic.config.LOGGING dictionary for configuration. The default configuration provides several predefined `handlers`: +By default, log_config parameter is set to use sanic.log.LOGGING_CONFIG_DEFAULTS dictionary for configuration. -- internal (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))
- For internal information console outputs. +There are three `loggers` used in sanic, and **must be defined if you want to create your own logging configuration**: - -- accessStream (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))
- For requests information logging in console - - -- errorStream (using [logging.StreamHandler](https://docs.python.org/3/library/logging.handlers.html#logging.StreamHandler))
- For error message and traceback logging in console. - - -- accessSysLog (using [logging.handlers.SysLogHandler](https://docs.python.org/3/library/logging.handlers.html#logging.handlers.SysLogHandler))
- For requests information logging to syslog. - Currently supports Windows (via localhost:514), Darwin (/var/run/syslog), - Linux (/dev/log) and FreeBSD (/dev/log).
- You would not be able to access this property if the directory doesn't exist. - (Notice that in Docker you have to enable everything by yourself) - - -- errorSysLog (using [logging.handlers.SysLogHandler](https://docs.python.org/3/library/logging.handlers.html#logging.handlers.SysLogHandler))
- For error message and traceback logging to syslog. - Currently supports Windows (via localhost:514), Darwin (/var/run/syslog), - Linux (/dev/log) and FreeBSD (/dev/log).
- You would not be able to access this property if the directory doesn't exist. - (Notice that in Docker you have to enable everything by yourself) - - -And `filters`: - -- accessFilter (using sanic.log.DefaultFilter)
- The filter that allows only levels in `DEBUG`, `INFO`, and `NONE(0)` - - -- errorFilter (using sanic.log.DefaultFilter)
- The filter that allows only levels in `WARNING`, `ERROR`, and `CRITICAL` - -There are two `loggers` used in sanic, and **must be defined if you want to create your own logging configuration**: - -- sanic:
+- root:
Used to log internal messages. +- sanic.error:
+ Used to log error logs. -- network:
- Used to log requests from network, and any information from those requests. +- sanic.access:
+ Used to log access logs. #### Log format: From 5cabc9cff2dc023db0e0667cba232e30cc1dbd96 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Wed, 13 Sep 2017 18:16:58 -0700 Subject: [PATCH 18/38] update doc --- docs/sanic/logging.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sanic/logging.md b/docs/sanic/logging.md index 092df5c7..286282f0 100644 --- a/docs/sanic/logging.md +++ b/docs/sanic/logging.md @@ -54,7 +54,7 @@ There are three `loggers` used in sanic, and **must be defined if you want to cr #### Log format: In addition to default parameters provided by python (asctime, levelname, message), -Sanic provides additional parameters for network logger with accessFilter: +Sanic provides additional parameters for access logger with: - host (str)
request.ip From 730f7c5e41635d5ab7a78102f4c534acab758392 Mon Sep 17 00:00:00 2001 From: Yun Xu Date: Wed, 13 Sep 2017 18:30:38 -0700 Subject: [PATCH 19/38] add doc for customizing logging config --- docs/sanic/logging.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/sanic/logging.md b/docs/sanic/logging.md index 286282f0..49805d0e 100644 --- a/docs/sanic/logging.md +++ b/docs/sanic/logging.md @@ -20,6 +20,13 @@ if __name__ == "__main__": app.run(debug=True, access_log=True) ``` +To use your own logging config, simply use `logging.config.dictConfig`, or +pass `log_config` when you initialize `Sanic` app: + +```python +app = Sanic('test', log_config=LOGGING_CONFIG) +``` + And to close logging, simply assign access_log=False: ```python From eb1146c6b66827b6178980353e35e68c83f9619f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?W=C3=A8i=20C=C5=8Dngru=C3=AC?= Date: Thu, 14 Sep 2017 18:40:20 +0800 Subject: [PATCH 20/38] fix #763, sanic can't decode latin1 encoded header value --- sanic/server.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index f62ba654..4dedc650 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -189,10 +189,12 @@ class HttpProtocol(asyncio.Protocol): and int(value) > self.request_max_size: exception = PayloadTooLarge('Payload Too Large') self.write_error(exception) - + try: + value = value.decode() + except UnicodeDecodeError: + value = value.decode('latin_1') self.headers.append( - (self._header_fragment.decode().casefold(), - value.decode())) + (self._header_fragment.decode().casefold(), value)) self._header_fragment = b'' From 12dafd07b87e3412f48cfb3672c1804b3879464c Mon Sep 17 00:00:00 2001 From: huyuhan Date: Fri, 15 Sep 2017 18:34:56 +0800 Subject: [PATCH 21/38] add __repr__ for sanic request --- sanic/request.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sanic/request.py b/sanic/request.py index 27ff011e..4e8a2e07 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -68,6 +68,11 @@ class Request(dict): self._cookies = None self.stream = None + def __repr__(self): + if self.method is None or not self._parsed_url: + return '<%s>' % self.__class__.__name__ + return '<%s: %s %r>' % (self.__class__.__name__, self.method, self.path) + @property def json(self): if self.parsed_json is None: From 77f70a0792eef25841039dcd1e2266f2d78e79f2 Mon Sep 17 00:00:00 2001 From: huyuhan Date: Fri, 15 Sep 2017 20:56:44 +0800 Subject: [PATCH 22/38] add __repr__ for sanic request --- sanic/request.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 4e8a2e07..fa80b47c 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -19,8 +19,9 @@ except ImportError: from sanic.exceptions import InvalidUsage from sanic.log import log - DEFAULT_HTTP_CONTENT_TYPE = "application/octet-stream" + + # HTTP/1.1: https://www.w3.org/Protocols/rfc2616/rfc2616-sec7.html#sec7.2.1 # > If the media type remains unknown, the recipient SHOULD treat it # > as type "application/octet-stream" @@ -69,9 +70,9 @@ class Request(dict): self.stream = None def __repr__(self): - if self.method is None or not self._parsed_url: + if self.method is None or not self.path: return '<%s>' % self.__class__.__name__ - return '<%s: %s %r>' % (self.__class__.__name__, self.method, self.path) + return '<%s: %s %s>' % (self.__class__.__name__, self.method, self.path) @property def json(self): @@ -175,8 +176,8 @@ class Request(dict): remote_addrs = [ addr for addr in [ addr.strip() for addr in forwarded_for - ] if addr - ] + ] if addr + ] if len(remote_addrs) > 0: self._remote_addr = remote_addrs[0] else: From f6eb35f67d6637fdd4b13e4d38ba17d4d21f1fb3 Mon Sep 17 00:00:00 2001 From: huyuhan Date: Fri, 15 Sep 2017 21:05:25 +0800 Subject: [PATCH 23/38] add __repr__ for sanic request --- sanic/request.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index fa80b47c..ea5c071e 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -71,8 +71,10 @@ class Request(dict): def __repr__(self): if self.method is None or not self.path: - return '<%s>' % self.__class__.__name__ - return '<%s: %s %s>' % (self.__class__.__name__, self.method, self.path) + return '<{class_name}>'.format(class_name=self.__class__.__name__) + return '<{class_name}: {method} {path}>'.format(class_name=self.__class__.__name__, + method=self.method, + path=self.path) @property def json(self): From 074d36eeba5a793b0c87becfda81603fcc5cb824 Mon Sep 17 00:00:00 2001 From: huyuhan Date: Fri, 15 Sep 2017 21:15:05 +0800 Subject: [PATCH 24/38] add __repr__ for sanic request --- sanic/request.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index ea5c071e..26b14fd9 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -71,10 +71,10 @@ class Request(dict): def __repr__(self): if self.method is None or not self.path: - return '<{class_name}>'.format(class_name=self.__class__.__name__) - return '<{class_name}: {method} {path}>'.format(class_name=self.__class__.__name__, - method=self.method, - path=self.path) + return '<{0}>'.format(self.__class__.__name__) + return '<{0}: {1} {2}>'.format(self.__class__.__name__, + self.method, + self.path) @property def json(self): From c836441a75eada7c50faa70f8ae682fc5c3d8704 Mon Sep 17 00:00:00 2001 From: Kuzma Leshakov Date: Mon, 18 Sep 2017 11:37:32 +0300 Subject: [PATCH 25/38] Update getting_started.md Hello World example at the main Readme file (https://github.com/channelcat/sanic/blob/master/README.rst) is different, it returns json. Here is returned text. In the following examples, such as Routing (http://sanic.readthedocs.io/en/latest/sanic/routing.html) is again used json. Therefore I suggest to make examples the same, having json as output --- docs/sanic/getting_started.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/sanic/getting_started.md b/docs/sanic/getting_started.md index 04d22248..3e89cc3e 100644 --- a/docs/sanic/getting_started.md +++ b/docs/sanic/getting_started.md @@ -9,15 +9,16 @@ syntax, so earlier versions of python won't work. ```python from sanic import Sanic - from sanic.response import text + from sanic.response import json - app = Sanic(__name__) + app = Sanic() @app.route("/") async def test(request): - return text('Hello world!') + return json({"hello": "world"}) - app.run(host="0.0.0.0", port=8000, debug=True) + if __name__ == "__main__": + app.run(host="0.0.0.0", port=8000) ``` 3. Run the server: `python3 main.py` From d8cebe1188bf03d424639b4f944c15f01dad668c Mon Sep 17 00:00:00 2001 From: lanf0n Date: Tue, 19 Sep 2017 18:14:25 +0800 Subject: [PATCH 26/38] to fix if platform is windows. --- sanic/config.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/sanic/config.py b/sanic/config.py index 0c2cc701..ec2dd5ed 100644 --- a/sanic/config.py +++ b/sanic/config.py @@ -1,6 +1,5 @@ import os import sys -import syslog import platform import types @@ -8,6 +7,11 @@ from sanic.log import DefaultFilter SANIC_PREFIX = 'SANIC_' +try: + from syslog import LOG_DAEMON +except ImportError: + LOG_DAEMON = 24 + _address_dict = { 'Windows': ('localhost', 514), 'Darwin': '/var/run/syslog', @@ -66,7 +70,7 @@ LOGGING = { 'class': 'logging.handlers.SysLogHandler', 'address': _address_dict.get(platform.system(), ('localhost', 514)), - 'facility': syslog.LOG_DAEMON, + 'facility': LOG_DAEMON, 'filters': ['accessFilter'], 'formatter': 'access' }, @@ -74,7 +78,7 @@ LOGGING = { 'class': 'logging.handlers.SysLogHandler', 'address': _address_dict.get(platform.system(), ('localhost', 514)), - 'facility': syslog.LOG_DAEMON, + 'facility': LOG_DAEMON, 'filters': ['errorFilter'], 'formatter': 'simple' }, From 1d719252cbc6ce79943e76d317cecd57b1872fe4 Mon Sep 17 00:00:00 2001 From: Hugh McNamara Date: Tue, 19 Sep 2017 14:58:49 +0100 Subject: [PATCH 27/38] use dependency injection to allow alternative json parser or encoder --- sanic/request.py | 4 ++-- sanic/response.py | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 27ff011e..aa778a8d 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -69,10 +69,10 @@ class Request(dict): self.stream = None @property - def json(self): + def json(self, loads=json_loads): if self.parsed_json is None: try: - self.parsed_json = json_loads(self.body) + self.parsed_json = loads(self.body) except Exception: if not self.body: return None diff --git a/sanic/response.py b/sanic/response.py index 3b0ef449..f661758b 100644 --- a/sanic/response.py +++ b/sanic/response.py @@ -237,7 +237,8 @@ class HTTPResponse(BaseHTTPResponse): def json(body, status=200, headers=None, - content_type="application/json", **kwargs): + content_type="application/json", dumps=json_dumps, + **kwargs): """ Returns response object with body in json format. @@ -246,7 +247,7 @@ def json(body, status=200, headers=None, :param headers: Custom Headers. :param kwargs: Remaining arguments that are passed to the json encoder. """ - return HTTPResponse(json_dumps(body, **kwargs), headers=headers, + return HTTPResponse(dumps(body, **kwargs), headers=headers, status=status, content_type=content_type) From a8f764c1612ab17eb99924ac2363c05110a1911d Mon Sep 17 00:00:00 2001 From: Hugh McNamara Date: Tue, 19 Sep 2017 18:12:53 +0100 Subject: [PATCH 28/38] make method instead of property for alternative json decoding of request --- sanic/request.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index aa778a8d..74200fe0 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -69,10 +69,10 @@ class Request(dict): self.stream = None @property - def json(self, loads=json_loads): + def json(self): if self.parsed_json is None: try: - self.parsed_json = loads(self.body) + self.parsed_json = json_loads(self.body) except Exception: if not self.body: return None @@ -80,6 +80,16 @@ class Request(dict): return self.parsed_json + def load_json(self, loads=json_loads): + try: + self.parsed_json = loads(self.body) + except Exception: + if not self.body: + return None + raise InvalidUsage("Failed when parsing body as json") + + return self.parsed_json + @property def token(self): """Attempt to return the auth header token. From 5cef1634edd029e914d0184627e98bcb63198173 Mon Sep 17 00:00:00 2001 From: Hugh McNamara Date: Fri, 22 Sep 2017 10:19:15 +0100 Subject: [PATCH 29/38] use json_loads function in json property of request --- sanic/request.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/sanic/request.py b/sanic/request.py index 74200fe0..43ecc511 100644 --- a/sanic/request.py +++ b/sanic/request.py @@ -71,12 +71,7 @@ class Request(dict): @property def json(self): if self.parsed_json is None: - try: - self.parsed_json = json_loads(self.body) - except Exception: - if not self.body: - return None - raise InvalidUsage("Failed when parsing body as json") + self.load_json() return self.parsed_json From f96ab027677d525d1a690223d217c27cc05517e9 Mon Sep 17 00:00:00 2001 From: lixxu Date: Wed, 27 Sep 2017 09:59:49 +0800 Subject: [PATCH 30/38] set scheme to http if not provided --- sanic/app.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/sanic/app.py b/sanic/app.py index 3776c915..5d80b360 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -460,7 +460,10 @@ class Sanic: if external: if not scheme: - scheme = netloc[:8].split(':', 1)[0] + if ':' in netloc[:8]: + scheme = netloc[:8].split(':', 1)[0] + else: + scheme = 'http' if '://' in netloc[:8]: netloc = netloc.split('://', 1)[-1] From 91b2167ebac836b54205e7e9a031597bd3ea6ee6 Mon Sep 17 00:00:00 2001 From: Adam Hopkins Date: Wed, 27 Sep 2017 11:07:06 +0300 Subject: [PATCH 31/38] Update extensions.md Add - [JWT](https://github.com/ahopkins/sanic-jwt): Authentication extension for JSON Web Tokens (JWT) extension package. --- docs/sanic/extensions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sanic/extensions.md b/docs/sanic/extensions.md index 5643f4fc..ad9b8156 100644 --- a/docs/sanic/extensions.md +++ b/docs/sanic/extensions.md @@ -7,6 +7,7 @@ A list of Sanic extensions created by the community. - [CORS](https://github.com/ashleysommer/sanic-cors): A port of flask-cors. - [Compress](https://github.com/subyraman/sanic_compress): Allows you to easily gzip Sanic responses. A port of Flask-Compress. - [Jinja2](https://github.com/lixxu/sanic-jinja2): Support for Jinja2 template. +- [JWT](https://github.com/ahopkins/sanic-jwt): Authentication extension for JSON Web Tokens (JWT). - [OpenAPI/Swagger](https://github.com/channelcat/sanic-openapi): OpenAPI support, plus a Swagger UI. - [Pagination](https://github.com/lixxu/python-paginate): Simple pagination support. - [Motor](https://github.com/lixxu/sanic-motor): Simple motor wrapper. From 9aec5febb8fc33972f0ff13bed9c11e6865ec4ab Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Wed, 27 Sep 2017 01:24:43 -0700 Subject: [PATCH 32/38] support vhosts in static routes --- sanic/app.py | 4 ++-- sanic/static.py | 4 ++-- tests/test_static.py | 17 +++++++++++++++++ 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/sanic/app.py b/sanic/app.py index 5d80b360..d553f09d 100644 --- a/sanic/app.py +++ b/sanic/app.py @@ -354,13 +354,13 @@ class Sanic: # Static Files def static(self, uri, file_or_directory, pattern=r'/?.+', use_modified_since=True, use_content_range=False, - stream_large_files=False, name='static'): + stream_large_files=False, name='static', host=None): """Register a root to serve files from. The input can either be a file or a directory. See """ static_register(self, uri, file_or_directory, pattern, use_modified_since, use_content_range, - stream_large_files, name) + stream_large_files, name, host) def blueprint(self, blueprint, **options): """Register a blueprint on the application. diff --git a/sanic/static.py b/sanic/static.py index a9683b27..1ebd7291 100644 --- a/sanic/static.py +++ b/sanic/static.py @@ -18,7 +18,7 @@ from sanic.response import file, file_stream, HTTPResponse def register(app, uri, file_or_directory, pattern, use_modified_since, use_content_range, - stream_large_files, name='static'): + stream_large_files, name='static', host=None): # TODO: Though sanic is not a file server, I feel like we should at least # make a good effort here. Modified-since is nice, but we could # also look into etags, expires, and caching @@ -122,4 +122,4 @@ def register(app, uri, file_or_directory, pattern, if not name.startswith('_static_'): name = '_static_{}'.format(name) - app.route(uri, methods=['GET', 'HEAD'], name=name)(_handler) + app.route(uri, methods=['GET', 'HEAD'], name=name, host=host)(_handler) diff --git a/tests/test_static.py b/tests/test_static.py index 091d63a4..6252b1c1 100644 --- a/tests/test_static.py +++ b/tests/test_static.py @@ -161,3 +161,20 @@ def test_static_content_range_error(file_name, static_file_directory): assert 'Content-Range' in response.headers assert response.headers['Content-Range'] == "bytes */%s" % ( len(get_file_content(static_file_directory, file_name)),) + + +@pytest.mark.parametrize('file_name', ['test.file', 'decode me.txt', 'python.png']) +def test_static_file(static_file_directory, file_name): + app = Sanic('test_static') + app.static( + '/testing.file', + get_file_path(static_file_directory, file_name), + host="www.example.com" + ) + + headers = {"Host": "www.example.com"} + request, response = app.test_client.get('/testing.file', headers=headers) + assert response.status == 200 + assert response.body == get_file_content(static_file_directory, file_name) + request, response = app.test_client.get('/testing.file') + assert response.status == 404 From 62871ec9b31345863f5c61e5b52c21c98cdabd32 Mon Sep 17 00:00:00 2001 From: lanf0n Date: Sat, 30 Sep 2017 01:16:26 +0800 Subject: [PATCH 33/38] add sphinx extension to add asyncio-specific markups --- docs/conf.py | 2 +- requirements-docs.txt | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index e254c183..7dd7462c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -25,7 +25,7 @@ import sanic # -- General configuration ------------------------------------------------ -extensions = ['sphinx.ext.autodoc'] +extensions = ['sphinx.ext.autodoc', 'sphinxcontrib.asyncio'] templates_path = ['_templates'] diff --git a/requirements-docs.txt b/requirements-docs.txt index efa74079..e12c1846 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -1,3 +1,4 @@ sphinx sphinx_rtd_theme recommonmark +sphinxcontrib-asyncio From e3852ceeca94317b11e0c24841d1678abf8806e1 Mon Sep 17 00:00:00 2001 From: Piotr Bulinski Date: Wed, 4 Oct 2017 12:50:57 +0200 Subject: [PATCH 34/38] Refactor access log for server --- sanic/server.py | 63 +++++++++++++++++++++---------------------------- 1 file changed, 27 insertions(+), 36 deletions(-) diff --git a/sanic/server.py b/sanic/server.py index 12efc180..e66afcf8 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -300,6 +300,28 @@ class HttpProtocol(asyncio.Protocol): # -------------------------------------------- # # Responding # -------------------------------------------- # + def log_response(self, response): + if self.has_log: + extra = { + 'status': getattr(response, 'status', 0), + } + + if isinstance(response, HTTPResponse): + extra['byte'] = len(response.body) + else: + extra['byte'] = -1 + + if self.request: + extra['host'] = '{0}:{1}'.format(self.request.ip[0], + self.request.ip[1]) + extra['request'] = '{0} {1}'.format(self.request.method, + self.request.url) + else: + extra['host'] = 'UNKNOWN' + extra['request'] = 'nil' + + netlog.info('', extra=extra) + def write_response(self, response): """ Writes response content synchronously to the transport. @@ -313,15 +335,7 @@ class HttpProtocol(asyncio.Protocol): response.output( self.request.version, keep_alive, self.keep_alive_timeout)) - if self.has_log: - netlog.info('', extra={ - 'status': response.status, - 'byte': len(response.body), - 'host': '{0}:{1}'.format(self.request.ip[0], - self.request.ip[1]), - 'request': '{0} {1}'.format(self.request.method, - self.request.url) - }) + self.log_response(response) except AttributeError: log.error( ('Invalid response object for url {}, ' @@ -360,15 +374,7 @@ class HttpProtocol(asyncio.Protocol): response.transport = self.transport await response.stream( self.request.version, keep_alive, self.keep_alive_timeout) - if self.has_log: - netlog.info('', extra={ - 'status': response.status, - 'byte': -1, - 'host': '{0}:{1}'.format(self.request.ip[0], - self.request.ip[1]), - 'request': '{0} {1}'.format(self.request.method, - self.request.url) - }) + self.log_response(response) except AttributeError: log.error( ('Invalid response object for url {}, ' @@ -414,24 +420,9 @@ class HttpProtocol(asyncio.Protocol): repr(e)), from_error=True ) finally: - if self.has_log: - extra = dict() - if isinstance(response, HTTPResponse): - extra['status'] = response.status - extra['byte'] = len(response.body) - else: - extra['status'] = 0 - extra['byte'] = -1 - if self.request: - extra['host'] = '%s:%d' % self.request.ip, - extra['request'] = '%s %s' % (self.request.method, - self.url) - else: - extra['host'] = 'UNKNOWN' - extra['request'] = 'nil' - if self.parser and not (self.keep_alive - and extra['status'] == 408): - netlog.info('', extra=extra) + if self.parser and (self.keep_alive + or getattr(response, 'status', 0) == 408): + self.log_response(response) self.transport.close() def bail_out(self, message, from_error=False): From 8ce749e339509e30e4e87bc854da4200cc2409c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Buli=C5=84ski?= Date: Thu, 5 Oct 2017 09:27:18 +0200 Subject: [PATCH 35/38] Update server.py --- sanic/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/server.py b/sanic/server.py index a17e479b..8f67901d 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -320,7 +320,7 @@ class HttpProtocol(asyncio.Protocol): extra['host'] = 'UNKNOWN' extra['request'] = 'nil' - netlog.info('', extra=extra) + access_logger.info('', extra=extra) def write_response(self, response): """ From 4b877e3f6bab0a754fa1907970e1b0842e824995 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Buli=C5=84ski?= Date: Thu, 5 Oct 2017 09:28:13 +0200 Subject: [PATCH 36/38] Update server.py --- sanic/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sanic/server.py b/sanic/server.py index 8f67901d..e151c54f 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -301,7 +301,7 @@ class HttpProtocol(asyncio.Protocol): # Responding # -------------------------------------------- # def log_response(self, response): - if self.has_log: + if self.access_log: extra = { 'status': getattr(response, 'status', 0), } From d876e3ed5c95116f65b3f244e4ce76ca4149dc29 Mon Sep 17 00:00:00 2001 From: Raphael Deem Date: Thu, 5 Oct 2017 22:20:36 -0700 Subject: [PATCH 37/38] fix false cookie encoding and output --- sanic/cookies.py | 3 ++- tests/test_cookies.py | 23 +++++++++++++++++++++-- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/sanic/cookies.py b/sanic/cookies.py index 16b798df..8ad8cbfc 100644 --- a/sanic/cookies.py +++ b/sanic/cookies.py @@ -98,7 +98,8 @@ class Cookie(dict): def __setitem__(self, key, value): if key not in self._keys: raise KeyError("Unknown cookie property") - return super().__setitem__(key, value) + if value is not False: + return super().__setitem__(key, value) def encode(self, encoding): output = ['%s=%s' % (self.key, _quote(self.value))] diff --git a/tests/test_cookies.py b/tests/test_cookies.py index d88288ee..84b493cb 100644 --- a/tests/test_cookies.py +++ b/tests/test_cookies.py @@ -25,6 +25,25 @@ def test_cookies(): assert response.text == 'Cookies are: working!' assert response_cookies['right_back'].value == 'at you' +@pytest.mark.parametrize("httponly,expected", [ + (False, False), + (True, True), +]) +def test_false_cookies_encoded(httponly, expected): + app = Sanic('test_text') + + @app.route('/') + def handler(request): + response = text('hello cookies') + response.cookies['hello'] = 'world' + response.cookies['hello']['httponly'] = httponly + return text(response.cookies['hello'].encode('utf8')) + + request, response = app.test_client.get('/') + + assert ('HttpOnly' in response.text) == expected + + @pytest.mark.parametrize("httponly,expected", [ (False, False), (True, True), @@ -34,7 +53,7 @@ def test_false_cookies(httponly, expected): @app.route('/') def handler(request): - response = text('Cookies are: {}'.format(request.cookies['test'])) + response = text('hello cookies') response.cookies['right_back'] = 'at you' response.cookies['right_back']['httponly'] = httponly return response @@ -43,7 +62,7 @@ def test_false_cookies(httponly, expected): response_cookies = SimpleCookie() response_cookies.load(response.headers.get('Set-Cookie', {})) - 'HttpOnly' in response_cookies == expected + assert ('HttpOnly' in response_cookies['right_back'].output()) == expected def test_http2_cookies(): app = Sanic('test_http2_cookies') From 4b3920daba560b6b318a525be437f16bf59fbe16 Mon Sep 17 00:00:00 2001 From: Max Murashov Date: Fri, 6 Oct 2017 16:53:30 +0300 Subject: [PATCH 38/38] Fix logs --- sanic/handlers.py | 20 +++++++++---------- sanic/server.py | 51 +++++++++++++++++++---------------------------- sanic/worker.py | 5 ++--- 3 files changed, 33 insertions(+), 43 deletions(-) diff --git a/sanic/handlers.py b/sanic/handlers.py index 2e335b09..9afcfb94 100644 --- a/sanic/handlers.py +++ b/sanic/handlers.py @@ -86,12 +86,13 @@ class ErrorHandler: self.log(format_exc()) if self.debug: url = getattr(request, 'url', 'unknown') - response_message = ( - 'Exception raised in exception handler "{}" ' - 'for uri: "{}"\n{}').format( - handler.__name__, url, format_exc()) - logger.error(response_message) - return text(response_message, 500) + response_message = ('Exception raised in exception handler ' + '"%s" for uri: "%s"\n%s') + logger.error(response_message, + handler.__name__, url, format_exc()) + + return text(response_message % ( + handler.__name__, url, format_exc()), 500) else: return text('An error occurred while handling an error', 500) return response @@ -114,10 +115,9 @@ class ErrorHandler: elif self.debug: html_output = self._render_traceback_html(exception, request) - response_message = ( - 'Exception occurred while handling uri: "{}"\n{}'.format( - request.url, format_exc())) - logger.error(response_message) + response_message = ('Exception occurred while handling uri: ' + '"%s"\n%s') + logger.error(response_message, request.url, format_exc()) return html(html_output, status=500) else: return html(INTERNAL_SERVER_ERROR_HTML, status=500) diff --git a/sanic/server.py b/sanic/server.py index e151c54f..8f60a864 100644 --- a/sanic/server.py +++ b/sanic/server.py @@ -337,15 +337,13 @@ class HttpProtocol(asyncio.Protocol): self.keep_alive_timeout)) self.log_response(response) except AttributeError: - logger.error( - ('Invalid response object for url {}, ' - 'Expected Type: HTTPResponse, Actual Type: {}').format( - self.url, type(response))) + logger.error('Invalid response object for url %s, ' + 'Expected Type: HTTPResponse, Actual Type: %s', + self.url, type(response)) self.write_error(ServerError('Invalid response type')) except RuntimeError: - logger.error( - 'Connection lost before response written @ {}'.format( - self.request.ip)) + logger.error('Connection lost before response written @ %s', + self.request.ip) except Exception as e: self.bail_out( "Writing response failed, connection closed {}".format( @@ -376,15 +374,13 @@ class HttpProtocol(asyncio.Protocol): self.request.version, keep_alive, self.keep_alive_timeout) self.log_response(response) except AttributeError: - logger.error( - ('Invalid response object for url {}, ' - 'Expected Type: HTTPResponse, Actual Type: {}').format( - self.url, type(response))) + logger.error('Invalid response object for url %s, ' + 'Expected Type: HTTPResponse, Actual Type: %s', + self.url, type(response)) self.write_error(ServerError('Invalid response type')) except RuntimeError: - logger.error( - 'Connection lost before response written @ {}'.format( - self.request.ip)) + logger.error('Connection lost before response written @ %s', + self.request.ip) except Exception as e: self.bail_out( "Writing response failed, connection closed {}".format( @@ -411,9 +407,8 @@ class HttpProtocol(asyncio.Protocol): version = self.request.version if self.request else '1.1' self.transport.write(response.output(version)) except RuntimeError: - logger.error( - 'Connection lost before error written @ {}'.format( - self.request.ip if self.request else 'Unknown')) + logger.error('Connection lost before error written @ %s', + self.request.ip if self.request else 'Unknown') except Exception as e: self.bail_out( "Writing error failed, connection closed {}".format( @@ -427,12 +422,10 @@ class HttpProtocol(asyncio.Protocol): def bail_out(self, message, from_error=False): if from_error or self.transport.is_closing(): - logger.error( - ("Transport closed @ {} and exception " - "experienced during error handling").format( - self.transport.get_extra_info('peername'))) - logger.debug( - 'Exception:\n{}'.format(traceback.format_exc())) + logger.error("Transport closed @ %s and exception " + "experienced during error handling", + self.transport.get_extra_info('peername')) + logger.debug('Exception:\n%s', traceback.format_exc()) else: exception = ServerError(message) self.write_error(exception) @@ -597,15 +590,14 @@ def serve(host, port, request_handler, error_handler, before_start=None, try: loop.add_signal_handler(_signal, loop.stop) except NotImplementedError: - logger.warn( - 'Sanic tried to use loop.add_signal_handler but it is' - ' not implemented on this platform.') + logger.warning('Sanic tried to use loop.add_signal_handler ' + 'but it is not implemented on this platform.') pid = os.getpid() try: - logger.info('Starting worker [{}]'.format(pid)) + logger.info('Starting worker [%s]', pid) loop.run_forever() finally: - logger.info("Stopping worker [{}]".format(pid)) + logger.info("Stopping worker [%s]", pid) # Run the on_stop function if provided trigger_events(before_stop, loop) @@ -667,8 +659,7 @@ def serve_multiple(server_settings, workers): server_settings['port'] = None def sig_handler(signal, frame): - logger.info("Received signal {}. Shutting down.".format( - Signals(signal).name)) + logger.info("Received signal %s. Shutting down.", Signals(signal).name) for process in processes: os.kill(process.pid, SIGINT) diff --git a/sanic/worker.py b/sanic/worker.py index 9f950c34..811c7e5c 100644 --- a/sanic/worker.py +++ b/sanic/worker.py @@ -142,9 +142,8 @@ class GunicornWorker(base.Worker): ) if self.max_requests and req_count > self.max_requests: self.alive = False - self.log.info( - "Max requests exceeded, shutting down: %s", self - ) + self.log.info("Max requests exceeded, shutting down: %s", + self) elif pid == os.getpid() and self.ppid != os.getppid(): self.alive = False self.log.info("Parent changed, shutting down: %s", self)