今回たまたまクライアント側でElmを使ったけど、これはElmに限ったことではない。
Client側での留意点#
url
はlocalhost:[port]
ではなくhttp://localhost:[port]
と指定しなければならない。つまり、URLにはちゃんとスキーム名を指定する。
Server側での留意点#
Access-Control-Allow-Origin
に関連するヘッダーをちゃんと設定する。
成功コード#
プログラムの内容#
サーバーは{ "msg" : "Hello, World!" }
という内容のJSONを送ってくるので、クライアントはその値を受け取って"Success: Hello, World!“を出力する。それだけ。
Client: Elm#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
| module Main exposing (..)
import Browser exposing (..)
import Json.Decode exposing (..)
import Http exposing (..)
import Html exposing (..)
import Html.Attributes exposing (..)
main = Browser.element
{ init = init
, update = update
, view = view
, subscriptions = subscriptions
}
type Model
= Loading
| Failed
| Success String
init : () -> (Model, Cmd Msg)
init _ =
( Loading, getServer )
type Msg
= GotData (Result Http.Error String)
update : Msg -> Model -> (Model, Cmd Msg)
update msg model =
case msg of
GotData result ->
case result of
Ok str ->
(Success str, Cmd.none)
Err _ ->
(Failed, Cmd.none)
getServer : Cmd Msg
getServer =
Http.get
{ url = "http://localhost:3000"
, expect = Http.expectJson GotData dataDecoder
}
dataDecoder : Decoder String
dataDecoder =
field "msg" string
view : Model -> Html Msg
view model =
case model of
Failed ->
p []
[ text "Failed!" ]
Loading ->
p []
[ text "Loading..." ]
Success str ->
p []
[ text ("Success : " ++ str) ]
subscriptions : Model -> Sub Msg
subscriptions _ = Sub.none
|
Server: JavaScript (Node.js)#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| const http = require('http');
const server = http.createServer();
server.on('request', (req, res) => {
res.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
});
const body = {
msg: 'Hello, World!'
};
res.write(JSON.stringify(body))
res.end();
});
server.listen(3000);
|
失敗と解決までの流れ#
Http.getの引数#
初めはサーバー側で次のようにしていた。
1
2
3
4
5
6
7
8
| server.on('request', (req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json' });
const body = {
msg: 'Hello, World!'
};
res.write(JSON.stringify(body))
res.end();
});
|
Elm側でのGetメソッドは次のように呼び出していた。
1
2
3
4
5
6
| getServer : Cmd Msg
getServer =
Http.get
{ url = "localhost:3000"
, expect = Http.expectJson GotData dataDecoder
}
|
すると、ブラウザ(Safari)のConsole上で次のようなエラーが出ていた。
1
2
| Cross origin requests are only supported for HTTP.
XMLHttpRequest cannot load localhost:3000 due to access control checks.
|
この時、「サーバー側はHTTPモジュールを使って通信してるのになんでonly supperted for HTTP
なんだ…?」と思った。
調べても情報が見つからないので、自分でちゃんと考えてみる。「HTTPだけに対応しています」というエラーが出ているということは、サーバがHTTP通信として認識されていないということ。ではHTTP通信として認識されるための条件とは何か。
とりあえずパッと思いついたのは「ポート番号」。HTTP通信は基本的に80番ポートで通信しているはずで、その時はポートを指定する必要が無い。
そこで、クライアント側では以下のように変えた。サーバ側ではポートを80番でlisten
することにした。
1
2
3
4
| Http.get
{ url = "localhost"
, expect = Http.expectJson GotData dataDecoder
}
|
今度は次のようなエラーが出た。
1
| Failed to load resource: the server responded with a status of 404 (Not Found) http://localhost:8000/localhost
|
意味不明なURLhttp://localhost:8000/localhost
にアクセスしようとしている…。elm reactor
で起動しているWebサーバのURLhttp://localhost:8000
とNode.js側のサーバのURLlocalhost
が重なってなんか変なことになっている。
ここで、ふと「エラーメッセージだとlocalhost
の前にhttp://
が付いているな、これを試しにつけてみよう」と閃く。
1
2
3
4
| Http.get
{ url = "http://localhost"
, expect = Http.expectJson GotData dataDecoder
}
|
すると、次のようなエラーに変わった。
1
2
3
| Origin http://localhost:8000 is not allowed by Access-Control-Allow-Origin.
XMLHttpRequest cannot load http://localhost/ due to access control checks.
Failed to load resource: Origin http://localhost:8000 is not allowed by Access-Control-Allow-Origin.
|
このエラーは以前別の機会で調べたことがあった。サーバ側で次のようにヘッダを付加してあげると解決。
1
2
3
4
| res.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Content-Type': 'application/json'
});
|
これでうまく動くようになった。ここで、「実はクライアント側の問題はhttp://
を付けていなかったことだけが原因?」と思い、次のように書き換えた。
1
2
3
4
| Http.get
{ url = "http://localhost:3000"
, expect = Http.expectJson GotData dataDecoder
}
|
サーバー側もポート3000でlisten
したところ、正常に動いた。
補足検証#
クライアント側をElmではなくJSでやってみる。
やっぱり以下のコードだとエラーが出た。
1
2
3
4
5
6
7
8
9
10
| fetch('localhost:3000')
.then(res => {
return res.json();
})
.then(data => {
return data.msg
})
.then(msg => {
console.log(msg)
})
|
これなら問題ない。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| fetch('http://localhost:3000')
.then(res => {
return res.json();
})
.then(data => {
return data.msg
})
.then(msg => {
console.log(msg)
})
// ちなみにasync/awaitで書くとこうなる
(async () => {
res = await fetch('http://localhost:3000');
data = await res.json();
console.log(await data.msg)
})()
|
結果の考察#
localhost
だとftp://localhost
ともhttp://localhost
とも取れてしまうから、確かに必要だ、と後で思った。
いつもブラウザのURL欄にlocalhost
と入力してアクセスできていたため、同じようにhttp://
を省いて通信できるものだと無意識に思っていた。しかしこれが通用するのはブラウザだけで、恐らく「スキーマが指定されていなかったらhttp://
をつけたものとみなす」という仕様なのだろう。