money_formatの小数点処理が謎

ふぅ…(仕事した顔) ここ数日でフォロワーさんのリクエストボックスを巡っていろいろ投稿してきた(*ノωノ) なかにはリクボのサーバーの調子が悪くて動いてない方もいたけど、やけに具体的なお題が最近届いたなって方は8割わたしです(*ノωノ)ヘヘ…
なぜかサーバー書くことになったからnodeでサーバー書いてるんだけどファイル保存、リクエスト、何するにも非同期。非同期地獄
USが対応してくれないんで、「俺のLaptop で」毎日10個くらいのバッチ処理が実行されてる。一台でいいからサーバーかVMをくれ、というリクエストは放置プレイ。 この現状を知った他チームの偉い人から「ユーのlaptop 、サーバーラックに入れようぜ」という謎の提案をもらう。
ポーリング 一定時間ごとにサーバーにリクエストを送り新着情報を監視する
好きな絵描きさんのお題箱にえっちな絵をリクエストしようと送信ボタンおしたらお題箱がサーバーエラーおこして発狂した
get sent の意味がわからないんだが、サーバーへのダウンロードリクエストのことなのかね うーん、わかんないや!
大量にサーバーにリクエスト送ってるけどどれくらいでパンクするのか見当つかない...😥😥
魔力はサーバーから供給して、魔法使いはサーバーに魔力をリクエストする方式?
どうみても、とんでもない負荷を生み出すAPIリクエストを出してくれと言われて、えっ、いいんです?扱うデータ量からして、1日経っても終わらないかもしれないんですよ?っていってもいいって言われたから強行してる。案の定1時間動き無し。サーバーが落ちてもしらんぞ私は……ヽ(^o^)丿

Google先生がなかなか教えてくれなかったので、穴埋め係させていただきます!
※Angular2/4 の前身の某JSのことを A……JS と伏せ字にしていますが、検索キーワードに引っかかってほしくないためです。別に裏の意図はありません……。

何がしたい?

Angular経験1ヶ月程度の初心者さんなのですが、ふつうにhttpモジュールでPOSTリクエストをしようとして、CORSで壮大にコケたというお話です。OPTIONSという謎のリクエストが走ったり。どうやって突破するのか?

CORS(Cross-Origin Resource Sharing)とは?

プレゼンテーション1.jpg

今見ているサイトとは違うサイトから、情報を取ってきたり渡したりしたらダメだよ、というブラウザのセキュリティ対策です。詳しくはこちらとか。→ HTTP アクセス制御 (CORS)
そもそも、フロントとバックが別のサイトを参照している、なんて不自然な状況かもしれませんが、開発段階で、フロントはローカル環境、バックは本サーバ(テストサーバ)という状況はよくあるんじゃないかと思うんです。

結論 ―― とりあえず解決する方法

理屈だとか、高度なセキュリティだとかは置いといて、とにかく動くようにしたい場合の結論ファースト。

フロント側 Angular の書き方

結局は、A……JS のドキュメントでも言われているとおり、リクエストヘッダに1文加える方法がカンタンです。Augular2/Angular4では、下記のようにします。

sample.component.ts
import { Component, OnInit } from '@angular/core';
import 'rxjs/add/operator/map'
import { Http, Headers } from "@angular/http"; // Http に加えて Headers をインポート

@Component({
  selector: 'app-sample',
  templateUrl: './sample.component.html',
  styleUrls: ['./sample.component.scss']
})
export class SampleComponent implements OnInit {

  headers :Headers;

  constructor( private http: Http ) {  
  }

  ngOnInit() {
    // POST で使うヘッダーを作っておく
    this.headers = new Headers();
    this.headers.append('Content-Type', 'application/x-www-form-urlencoded');
  }

  // 何かしらHTTPリクエストを走らせるイベントなど
  onSampleSubmitted( postParam :Array )
  {
    this.http.post('https://myhost.com/api/', postParam, {headers: headers} )
             .subscribe( response => console.log(response.json()) );
  }
}

サーバー側の書き方

CORS問題はフロント側だけは解決できません……。

htaccess に書く

APIのエンドポイント(上記で言うと /api ディレクトリ )に、下記を設置してしまうのがカンタンです。言語を選ばないし。

.htaccess
<IfModule mod_headers.c>
    Header set Access-Control-Allow-Origin *
</IfModule>

PHPでJSONを受け取る

PHPの場合は、これにもうひと工夫必要です。Angularから送られてくるPOST情報は、PHPが理解できるForm形式( key=value1&key2=value2 というURLパラメータの形式)ではなく、JSONです。なので、いつもの $_POST で受け取ろうとすると空っぽ。そこで次のようにする必要があります。

ExampleAPIController.php
    switch( $_SERVER["REQUEST_METHOD"] ){
      case 'POST':
        $json_string = file_get_contents('php://input');
        $posted_json = json_decode($json_string); // これを $_POST代わりに使う
        break;
    }

解説 ―― もう少しちゃんとした解決策

上記解決策はいくつか「キモチワルイ」ところがあります。それをちゃんと解決するために、実際にフロントとサーバーとどういうやり取りをしているのか、詳しく見ながら検証します。

Allow-Origin * 問題

せっかくCORSのために強力なセキュリティが用意されているのに、そのドアを「全開」にしておくのは褒められたものではありません。Angularであれば、基本的にフロントは特定のサーバー(この場合はローカルホスト)しかないので、Allow-Originもそのサーバーに限定します。

問題はどうやってそれを書くか? サブドメインは? http は要るの要らないの?

実は難しく考える必要はありません。先の .htaccess を一旦削除してAPIアクセスをして、ブラウザのコンソールを見てください。

名称未設定-2.jpg

親切に「Access-Control-Allow-Origin で http://localhost:4200 が許可されていません」と表示されています。それをそのまま書きます。

.htaccess
<IfModule mod_headers.c>
    Header set Access-Control-Allow-Origin http://localhost:4200
</IfModule>

httpだけでなく、ポート番号まで要るとは思いませんでした……。よく考えたら、これはブラウザが単純に文字列判定するためのもので、サーバー側からアクセスできるかどうかは関係ない、ということなのかもしれません。

Content-Type: form-urlencoded 問題

そもそもなぜ application/x-www-form-urlencoded にするのか?というと、こうしておくと、ブラウザがCORSのための「プリフライトリクエスト」を行わないため、とあります。わかりやすく言うと、CORSのセキュリティを1つ省略する=ちょっとセキュリティが弱くなるということ。
もう1つ問題があって、リクエストヘッダで「コンテンツはFORMだ」と言ってるのに実際はJSONと、宣言と中身が違うこと。PHPなどの受け取り側はあまり気にしてないので違ってても支障はありませんが、礼儀として、中身がJSONなら Content-Type も application/json としたいところです。

下記が、別のツールを使って正しい form-urlencoded と json のリクエストを作ってみたところ。宣言と中身が一致しているリクエストです。

いつものブラウザが送っているリクエスト
content-length: 21
content-type: application/x-www-form-urlencoded # FORM
key1=value1&key2=value2                         # FORM
上記プログラムでAngularが送ろうとするリクエスト
content-length: 33
content-type: application/x-www-form-urlencoded # FORMなのに
{"key1":"value1","key2":"value2"}               # JSON
こうしたい
content-length: 33
content-type: application/json    # JSON!
{"key1":"value1","key2":"value2"} # JSON!!

プリフライトリクエスト OPTIONS に応答する

結論を先に言うと、難しいことを考える必要はなくて、.htaccess に必要なことを書いてあげるだけです。

<IfModule mod_headers.c>
    Header set Access-Control-Allow-Origin http://localhost:4200
    Header set Access-Control-Allow-Headers content-type
    Header set Access-Control-Allow-Methods GET, POST, PUT, DELETE
</IfModule>

Angular を修正

サーバーの準備できたら、Angularも修正します。
下記のようにすると、OPTIONSのプリフライトリクエストが走るようになりますが、サーバーからちゃんと許可が降りると、その後すぐにPOSTが走ります。

sample.component.ts

  ngOnInit() {
    this.headers = new Headers();
    // this.headers.append('Content-Type', 'application/x-www-form-urlencoded');
    this.headers.append('Content-Type', 'application/json');
  }

プリフライトリクエスト OPTIONS とは?

プリフライトリクエストとは、ブラウザが、CORSにあたるとき、別サーバーに対して何が許可されているかを実際のリクエストをする前に確認するリクエストで、実際にPOSTの前にOPTIONSというリクエストを飛ばします。
このOPTIONSというリクエストに対して、Allow-Methods として POST が返ってきたら、本番の POST をリクエストするという仕組みです。

プレゼンテーション1.png

補足 やらなくていいこと

OPTIONS を含める

許可メソッドに OPTIONS を含める必要はありません。
OPTIONS 以外に何が許可されているかを問い合わせるのが OPTIONS なので、OPTIONS がなくても動きます。

こうする必要はない
<IfModule mod_headers.c>
    Header set Access-Control-Allow-Origin http://localhost:4200
    Header set Access-Control-Allow-Headers content-type
    Header set Access-Control-Allow-Methods GET, POST, PUT, DELETE, OPTIONS
</IfModule>

と書いたところで……。どうも Allow-Methods 自体も必要ないようです。これは、上の図のように、サーバーが Allow-Methodsを返してフロントで判断するのではなく、その前にサーバーが Request-Methods を見て許可するか判断しろ、ということなのかも。

これでも動く
<IfModule mod_headers.c>
    Header set Access-Control-Allow-Origin http://localhost:4200
    Header set Access-Control-Allow-Headers content-type
</IfModule>

OPTIONSのリクエストに明示的に応える

例えばPHPのプログラム内で、MethodがOPTIONSの場合の処理を用意する必要はありません。.htaccessの応答だけで十分。仮に何かを出力しても、OPTIONSの応答では、ブラウザは本文を無視します。
我が家では下記のように、なにもしないことを明記しています。

ExampleAPIController.php
    switch( $_SERVER["REQUEST_METHOD"] ){
      case 'POST':
        $json_string = file_get_contents('php://input');
        $posted_json = json_decode($json_string); // これを $_POST代わりに使う
        break;

      case 'OPTIONS':
        exit;
    }

感想

というわけで、AngularでPOSTしたかっただけなのにどっぷりハマって、HTTPのリクエストと応答の仕組みまでいろいろ勉強させていただきました。FORM形式とJSONの違いとか、JSONだとPHPは認識しないなど、知らなかったこともわかったし、クライアントとサーバー間のやりとりを、もう少しディープに書くことができるようになった、気がします。

[紹介元] PHPタグが付けられた新着投稿 – Qiita money_formatの小数点処理が謎

  • コメント

    1. 匿名希望
      2017/09/15(金) 02:30:49

      ふぅ…(仕事した顔)
      ここ数日でフォロワーさんのリクエストボックスを巡っていろいろ投稿してきた(*ノωノ)
      なかにはリクボのサーバーの調子が悪くて動いてない方もいたけど、やけに具体的なお題が最近届いたなって方は8割わたしです(*ノωノ)ヘヘ…

    2. 匿名希望
      2017/09/15(金) 02:30:49

      なぜかサーバー書くことになったからnodeでサーバー書いてるんだけどファイル保存、リクエスト、何するにも非同期。非同期地獄

    3. 匿名希望
      2017/09/15(金) 02:30:49

      USが対応してくれないんで、「俺のLaptop で」毎日10個くらいのバッチ処理が実行されてる。一台でいいからサーバーかVMをくれ、というリクエストは放置プレイ。

      この現状を知った他チームの偉い人から「ユーのlaptop 、サーバーラックに入れようぜ」という謎の提案をもらう。

    4. 匿名希望
      2017/09/15(金) 02:30:49

      ポーリング
      一定時間ごとにサーバーにリクエストを送り新着情報を監視する

    5. 匿名希望
      2017/09/15(金) 02:30:49

      好きな絵描きさんのお題箱にえっちな絵をリクエストしようと送信ボタンおしたらお題箱がサーバーエラーおこして発狂した

    記事に戻る

関連記事